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 combined_pubkey = musig::combine_keys([self.user_pubkey, self.server_pubkey])
390 .x_only_public_key().0;
391 let expiry_policy = ServerVtxoPolicy::new_expiry(combined_pubkey);
392 vec![
393 Vtxo {
394 policy: expiry_policy,
395 amount: amount,
396 expiry_height: self.expiry_height,
397 server_pubkey: self.server_pubkey,
398 exit_delta: self.exit_delta,
399 anchor_point: self.utxo.expect("state invariant"),
400 genesis: Full { items: vec![] },
401 point: self.utxo.expect("state invariant"),
402 },
403 Vtxo {
404 policy: ServerVtxoPolicy::User(VtxoPolicy::new_pubkey(self.user_pubkey)),
405 amount: amount - fee,
406 expiry_height: self.expiry_height,
407 server_pubkey: self.server_pubkey,
408 exit_delta: self.exit_delta,
409 anchor_point: self.utxo.expect("state invariant"),
410 genesis: Full {
411 items: vec![
412 GenesisItem {
413 transition: GenesisTransition::new_arkoor(
414 vec![self.user_pubkey],
415 tap_tweak,
416 None,
417 ),
418 output_idx: 0,
419 other_outputs: vec![],
420 fee_amount: fee,
421 }
422 ],
423 },
424 point: OutPoint::new(exit_txid, BOARD_FUNDING_TX_VTXO_VOUT),
425 },
426 ]
427 }
428
429 pub fn spend_info(&self) -> Vec<(VtxoId, Txid)> {
431 let exit_txid = self.exit_data.as_ref().expect("state invariant").txid;
432 vec![(self.utxo.expect("state invariant").into(), exit_txid)]
433 }
434}
435
436impl<S: state::CanSign> BoardBuilder<S> {
437 pub fn user_pub_nonce(&self) -> &musig::PublicNonce {
438 self.user_pub_nonce.as_ref().expect("state invariant")
439 }
440}
441
442impl BoardBuilder<state::ServerCanCosign> {
443 pub fn new_for_cosign(
446 user_pubkey: PublicKey,
447 expiry_height: BlockHeight,
448 server_pubkey: PublicKey,
449 exit_delta: BlockDelta,
450 amount: Amount,
451 fee: Amount,
452 utxo: OutPoint,
453 user_pub_nonce: musig::PublicNonce,
454 ) -> BoardBuilder<state::ServerCanCosign> {
455 let exit_data = compute_exit_data(
456 user_pubkey, server_pubkey, expiry_height, exit_delta, amount, fee, utxo,
457 );
458
459 BoardBuilder {
460 user_pubkey, expiry_height, server_pubkey, exit_delta,
461 amount: Some(amount),
462 fee: Some(fee),
463 utxo: Some(utxo),
464 user_pub_nonce: Some(user_pub_nonce),
465 user_sec_nonce: None,
466 exit_data: Some(exit_data),
467 _state: PhantomData,
468 }
469 }
470
471 pub fn server_cosign(&self, key: &Keypair) -> BoardCosignResponse {
475 let exit_data = self.exit_data.as_ref().expect("state invariant");
476 let sighash = exit_data.sighash;
477 let taproot = &exit_data.funding_taproot;
478 let (pub_nonce, partial_signature) = musig::deterministic_partial_sign(
479 key,
480 [self.user_pubkey],
481 &[&self.user_pub_nonce()],
482 sighash.to_byte_array(),
483 Some(taproot.tap_tweak().to_byte_array()),
484 );
485 BoardCosignResponse { pub_nonce, partial_signature }
486 }
487}
488
489impl BoardBuilder<state::CanFinish> {
490 pub fn verify_cosign_response(&self, server_cosign: &BoardCosignResponse) -> bool {
492 let exit_data = self.exit_data.as_ref().expect("state invariant");
493 let sighash = exit_data.sighash;
494 let taproot = &exit_data.funding_taproot;
495 scripts::verify_partial_sig(
496 sighash,
497 taproot.tap_tweak(),
498 (self.server_pubkey, &server_cosign.pub_nonce),
499 (self.user_pubkey, self.user_pub_nonce()),
500 &server_cosign.partial_signature
501 )
502 }
503
504 pub fn build_vtxo(
506 mut self,
507 server_cosign: &BoardCosignResponse,
508 user_key: &Keypair,
509 ) -> Result<Vtxo<Full>, IncorrectSigningKeyError> {
510 if user_key.public_key() != self.user_pubkey {
511 return Err(IncorrectSigningKeyError {
512 required: Some(self.user_pubkey),
513 provided: user_key.public_key(),
514 });
515 }
516
517 let exit_data = self.exit_data.as_ref().expect("state invariant");
518 let sighash = exit_data.sighash;
519 let taproot = &exit_data.funding_taproot;
520 let exit_txid = exit_data.txid;
521
522 let agg_nonce = musig::nonce_agg(&[&self.user_pub_nonce(), &server_cosign.pub_nonce]);
523 let (user_sig, final_sig) = musig::partial_sign(
524 [self.user_pubkey, self.server_pubkey],
525 agg_nonce,
526 user_key,
527 self.user_sec_nonce.take().expect("state invariant"),
528 sighash.to_byte_array(),
529 Some(taproot.tap_tweak().to_byte_array()),
530 Some(&[&server_cosign.partial_signature]),
531 );
532 debug_assert!(
533 scripts::verify_partial_sig(
534 sighash,
535 taproot.tap_tweak(),
536 (self.user_pubkey, self.user_pub_nonce()),
537 (self.server_pubkey, &server_cosign.pub_nonce),
538 &user_sig,
539 ),
540 "invalid board partial exit tx signature produced",
541 );
542
543 let final_sig = final_sig.expect("we provided the other sig");
544 debug_assert!(
545 SECP.verify_schnorr(
546 &final_sig, &sighash.into(), &taproot.output_key().to_x_only_public_key(),
547 ).is_ok(),
548 "invalid board exit tx signature produced",
549 );
550
551 let amount = self.amount.expect("state invariant");
552 let fee = self.fee.expect("state invariant");
553 let vtxo_amount = amount.checked_sub(fee).expect("fee cannot exceed amount");
554
555 Ok(Vtxo {
556 amount: vtxo_amount,
557 expiry_height: self.expiry_height,
558 server_pubkey: self.server_pubkey,
559 exit_delta: self.exit_delta,
560 anchor_point: self.utxo.expect("state invariant"),
561 genesis: Full {
562 items: vec![GenesisItem {
563 transition: GenesisTransition::new_cosigned(
564 vec![self.user_pubkey, self.server_pubkey],
565 Some(final_sig),
566 ),
567 output_idx: 0,
568 other_outputs: vec![],
569 fee_amount: fee,
570 }],
571 },
572 policy: VtxoPolicy::new_pubkey(self.user_pubkey),
573 point: OutPoint::new(exit_txid, BOARD_FUNDING_TX_VTXO_VOUT),
574 })
575 }
576}
577
578#[derive(Debug, Clone, thiserror::Error)]
579#[error("board funding tx validation error: {0}")]
580pub struct BoardFundingTxValidationError(String);
581
582
583#[cfg(test)]
584mod test {
585 use std::str::FromStr;
586
587 use bitcoin::{absolute, transaction, Amount};
588
589 use crate::test_util::encoding_roundtrip;
590
591 use super::*;
592
593 #[test]
594 fn test_board_builder() {
595 let user_key = Keypair::from_str("5255d132d6ec7d4fc2a41c8f0018bb14343489ddd0344025cc60c7aa2b3fda6a").unwrap();
599 let server_key = Keypair::from_str("1fb316e653eec61de11c6b794636d230379509389215df1ceb520b65313e5426").unwrap();
600
601 let amount = Amount::from_btc(1.5).unwrap();
603 let fee = Amount::from_btc(0.1).unwrap();
604 let expiry = 100_000;
605 let server_pubkey = server_key.public_key();
606 let exit_delta = 24;
607 let builder = BoardBuilder::new(
608 user_key.public_key(), expiry, server_pubkey, exit_delta,
609 );
610 let funding_tx = Transaction {
611 version: transaction::Version::TWO,
612 lock_time: absolute::LockTime::ZERO,
613 input: vec![],
614 output: vec![TxOut {
615 value: amount,
616 script_pubkey: builder.funding_script_pubkey(),
617 }],
618 };
619 let utxo = OutPoint::new(funding_tx.compute_txid(), 0);
620 assert_eq!(utxo.to_string(), "8c4b87af4ce8456bbd682859959ba64b95d5425d761a367f4f20b8ffccb1bde0:0");
621 let builder = builder.set_funding_details(amount, fee, utxo).unwrap().generate_user_nonces();
622
623 let cosign = {
625 let server_builder = BoardBuilder::new_for_cosign(
626 builder.user_pubkey, expiry, server_pubkey, exit_delta, amount, fee, utxo, *builder.user_pub_nonce(),
627 );
628 server_builder.server_cosign(&server_key)
629 };
630
631 assert!(builder.verify_cosign_response(&cosign));
633 let vtxo = builder.build_vtxo(&cosign, &user_key).unwrap();
634
635 encoding_roundtrip(&vtxo);
636
637 vtxo.validate(&funding_tx).unwrap();
638 }
639
640 fn create_board_vtxo() -> (Vtxo<Full>, Transaction, Keypair, Keypair) {
642 let user_key = Keypair::from_str("5255d132d6ec7d4fc2a41c8f0018bb14343489ddd0344025cc60c7aa2b3fda6a").unwrap();
643 let server_key = Keypair::from_str("1fb316e653eec61de11c6b794636d230379509389215df1ceb520b65313e5426").unwrap();
644
645 let amount = Amount::from_btc(1.5).unwrap();
646 let fee = Amount::from_btc(0.1).unwrap();
647 let expiry = 100_000;
648 let server_pubkey = server_key.public_key();
649 let exit_delta = 24;
650
651 let builder = BoardBuilder::new(
652 user_key.public_key(), expiry, server_pubkey, exit_delta,
653 );
654 let funding_tx = Transaction {
655 version: transaction::Version::TWO,
656 lock_time: absolute::LockTime::ZERO,
657 input: vec![],
658 output: vec![TxOut {
659 value: amount,
660 script_pubkey: builder.funding_script_pubkey(),
661 }],
662 };
663 let utxo = OutPoint::new(funding_tx.compute_txid(), 0);
664 let builder = builder.set_funding_details(amount, fee, utxo).unwrap().generate_user_nonces();
665
666 let cosign = {
667 let server_builder = BoardBuilder::new_for_cosign(
668 builder.user_pubkey, expiry, server_pubkey, exit_delta, amount, fee, utxo, *builder.user_pub_nonce(),
669 );
670 server_builder.server_cosign(&server_key)
671 };
672
673 let vtxo = builder.build_vtxo(&cosign, &user_key).unwrap();
674 (vtxo, funding_tx, user_key, server_key)
675 }
676
677 #[test]
678 fn test_new_from_vtxo_success() {
679 let (vtxo, funding_tx, _, server_key) = create_board_vtxo();
680
681 vtxo.validate(&funding_tx).unwrap();
682 println!("amount: {}", vtxo.amount());
683
684 let builder = BoardBuilder::new_from_vtxo(&vtxo, &funding_tx, server_key.public_key())
686 .expect("Is valid");
687
688 let server_vtxos = builder.build_internal_unsigned_vtxos();
689 assert_eq!(server_vtxos.len(), 2);
690 assert!(matches!(server_vtxos[0].policy(), ServerVtxoPolicy::Expiry(..)));
691 assert!(matches!(server_vtxos[1].policy(), ServerVtxoPolicy::User(VtxoPolicy::Pubkey {..})));
692 assert_eq!(server_vtxos[1].id(), vtxo.id());
693 assert_eq!(server_vtxos[1].txout(), vtxo.txout());
694 assert_eq!(server_vtxos[0].txout(), funding_tx.output[0]);
695 assert_eq!(
696 server_vtxos[1].transactions().nth(0).unwrap().tx.compute_txid(),
697 vtxo.transactions().nth(0).unwrap().tx.compute_txid(),
698 );
699 }
700
701 #[test]
702 fn test_new_from_vtxo_txid_mismatch() {
703 let (vtxo, funding_tx, _, server_key) = create_board_vtxo();
704
705 let wrong_funding_tx = Transaction {
707 version: transaction::Version::TWO,
708 lock_time: absolute::LockTime::ZERO,
709 input: vec![],
710 output: vec![TxOut {
711 value: Amount::from_btc(2.0).unwrap(), script_pubkey: funding_tx.output[0].script_pubkey.clone(),
713 }],
714 };
715
716 let result = BoardBuilder::new_from_vtxo(&vtxo, &wrong_funding_tx, server_key.public_key());
717 assert!(matches!(
718 result,
719 Err(BoardFromVtxoError::FundingTxMismatch { expected, got })
720 if expected == vtxo.chain_anchor().txid && got == wrong_funding_tx.compute_txid()
721 ));
722 }
723
724 #[test]
725 fn test_new_from_vtxo_server_pubkey_mismatch() {
726 let (vtxo, funding_tx, _, _) = create_board_vtxo();
727
728 let wrong_server_key = Keypair::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
730
731 let result = BoardBuilder::new_from_vtxo(&vtxo, &funding_tx, wrong_server_key.public_key());
732 assert!(matches!(
733 result,
734 Err(BoardFromVtxoError::ServerPubkeyMismatch { expected, got })
735 if expected == wrong_server_key.public_key() && got == vtxo.server_pubkey()
736 ));
737 }
738
739 #[test]
740 fn test_new_from_vtxo_vtxoid_mismatch() {
741 let (mut vtxo, funding_tx, _, server_key) = create_board_vtxo();
749
750 let original_point = vtxo.point;
752 vtxo.point = OutPoint::new(vtxo.point.txid, vtxo.point.vout + 1);
753
754 let result = BoardBuilder::new_from_vtxo(&vtxo, &funding_tx, server_key.public_key());
755 assert!(matches!(
756 result,
757 Err(BoardFromVtxoError::VtxoIdMismatch { expected, got })
758 if expected == original_point && got == vtxo.point
759 ));
760 }
761
762 #[test]
763 fn test_board_funding_error() {
764 fn new_builder_with_funding_details(amount: Amount, fee: Amount) -> Result<BoardBuilder<state::CanGenerateNonces>, BoardFundingError> {
765 let user_key = Keypair::from_str("5255d132d6ec7d4fc2a41c8f0018bb14343489ddd0344025cc60c7aa2b3fda6a").unwrap();
766 let server_key = Keypair::from_str("1fb316e653eec61de11c6b794636d230379509389215df1ceb520b65313e5426").unwrap();
767 let expiry = 100_000;
768 let server_pubkey = server_key.public_key();
769 let exit_delta = 24;
770 let builder = BoardBuilder::new(
771 user_key.public_key(), expiry, server_pubkey, exit_delta,
772 );
773 let funding_tx = Transaction {
774 version: transaction::Version::TWO,
775 lock_time: absolute::LockTime::ZERO,
776 input: vec![],
777 output: vec![TxOut {
778 value: amount,
779 script_pubkey: builder.funding_script_pubkey(),
780 }],
781 };
782 let utxo = OutPoint::new(funding_tx.compute_txid(), 0);
783 builder.set_funding_details(amount, fee, utxo)
784 }
785
786 let fee = Amount::ONE_BTC;
787
788 let zero_amount_err = new_builder_with_funding_details(Amount::ZERO, fee).err();
789 assert_eq!(zero_amount_err, Some(BoardFundingError::ZeroAmount));
790
791 let fee_higher_err = new_builder_with_funding_details(Amount::ONE_SAT, fee).err();
792 assert_eq!(fee_higher_err, Some(BoardFundingError::FeeHigherThanAmount { amount: Amount::ONE_SAT, fee }));
793
794 let zero_amount_after_fee_err = new_builder_with_funding_details(fee, fee).err();
795 assert_eq!(zero_amount_after_fee_err, Some(BoardFundingError::ZeroAmountAfterFee { amount: fee, fee }));
796 }
797}
798