1use std::marker::PhantomData;
68
69use bitcoin::hashes::Hash;
70use bitcoin::{Amount, OutPoint, TapSighash, Transaction, Txid, TxIn, TxOut, ScriptBuf, Sequence, Witness};
71use bitcoin::taproot::TapTweakHash;
72use bitcoin::secp256k1::{schnorr, Keypair, PublicKey};
73use bitcoin_ext::{P2TR_DUST, TxOutExt, fee};
74use secp256k1_musig::musig::PublicNonce;
75
76use crate::vtxo::{GenesisItem, GenesisTransition};
77use crate::arkoor::arkoor_sighash;
78use crate::{Vtxo, VtxoId};
79use crate::VtxoRequest;
80use crate::scripts;
81use crate::musig;
82use crate::vtxo::VtxoPolicy;
83
84#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
85pub enum ArkoorConstructionError {
86 #[error("Input amount of {input} does not match output amount of {output}")]
87 Unbalanced {
88 input: Amount,
89 output: Amount,
90 },
91 #[error("An output is below the dust threshold")]
92 Dust,
93 #[error("Too many inputs provided")]
94 TooManyInputs,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
98pub enum ArkoorSigningError {
99 #[error("An error occurred while building arkoor: {0}")]
100 ArkoorConstructionError(ArkoorConstructionError),
101 #[error("Wrong number of user nonces provided. Expected {expected}, got {got}")]
102 InvalidNbUserNonces {
103 expected: usize,
104 got: usize,
105 },
106 #[error("Wrong number of server nonces provided. Expected {expected}, got {got}")]
107 InvalidNbServerNonces {
108 expected: usize,
109 got: usize,
110 },
111 #[error("Incorrect signing key provided. Expected {expected}, got {got}")]
112 IncorrectKey {
113 expected: PublicKey,
114 got: PublicKey,
115 },
116 #[error("Wrong number of server partial sigs. Expected {expected}, got {got}")]
117 InvalidNbServerPartialSigs {
118 expected: usize,
119 got: usize
120 },
121 #[error("Invalid partial signature at index {index}")]
122 InvalidPartialSignature {
123 index: usize,
124 },
125 #[error("Wrong number of packages. Expected {expected}, got {got}")]
126 InvalidNbPackages {
127 expected: usize,
128 got: usize,
129 },
130 #[error("Wrong number of keypairs. Expected {expected}, got {got}")]
131 InvalidNbKeypairs {
132 expected: usize,
133 got: usize,
134 },
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct CosignResponse {
139 pub server_pub_nonces: Vec<musig::PublicNonce>,
140 pub server_partial_sigs: Vec<musig::PartialSignature>,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct CosignRequest<V> {
145 pub user_pub_nonces: Vec<musig::PublicNonce>,
146 pub input: V,
147 pub outputs: Vec<VtxoRequest>,
148 pub dust_outputs: Vec<VtxoRequest>,
149 pub use_checkpoint: bool,
150}
151
152impl<V> CosignRequest<V> {
153 pub fn new(
154 user_pub_nonces: Vec<musig::PublicNonce>,
155 input: V,
156 outputs: Vec<VtxoRequest>,
157 dust_outputs: Vec<VtxoRequest>,
158 use_checkpoint: bool,
159 ) -> Self {
160 Self {
161 user_pub_nonces,
162 input,
163 outputs,
164 dust_outputs,
165 use_checkpoint,
166 }
167 }
168}
169
170impl CosignRequest<VtxoId> {
171 pub fn with_vtxo(self, vtxo: Vtxo) -> Result<CosignRequest<Vtxo>, &'static str> {
172 if self.input != vtxo.id() {
173 return Err("Input vtxo id does not match the provided vtxo id")
174 }
175
176 Ok(CosignRequest::new(
177 self.user_pub_nonces,
178 vtxo,
179 self.outputs,
180 self.dust_outputs,
181 self.use_checkpoint,
182 ))
183 }
184}
185
186
187pub mod state {
188 mod sealed {
196 pub trait Sealed {}
197 impl Sealed for super::Initial {}
198 impl Sealed for super::UserGeneratedNonces {}
199 impl Sealed for super::UserSigned {}
200 impl Sealed for super::ServerCanCosign {}
201 impl Sealed for super::ServerSigned {}
202 }
203
204 pub trait BuilderState: sealed::Sealed {}
205
206 pub struct Initial;
208 impl BuilderState for Initial {}
209
210 pub struct UserGeneratedNonces;
212 impl BuilderState for UserGeneratedNonces {}
213
214 pub struct UserSigned;
216 impl BuilderState for UserSigned {}
217
218 pub struct ServerCanCosign;
220 impl BuilderState for ServerCanCosign {}
221
222
223 pub struct ServerSigned;
225 impl BuilderState for ServerSigned {}
226}
227
228pub struct CheckpointedArkoorBuilder<S: state::BuilderState> {
229 input: Vtxo,
232 outputs: Vec<VtxoRequest>,
235 dust_outputs: Vec<VtxoRequest>,
237
238 checkpoint_data: Option<(Transaction, Txid, TapTweakHash)>,
243 unsigned_arkoor_txs: Vec<Transaction>,
245 unsigned_dust_fanout_tx: Option<Transaction>,
248 unsigned_dust_exit_txs: Option<Vec<Transaction>>,
251 sighashes: Vec<TapSighash>,
253 arkoor_taptweak: TapTweakHash,
255 new_vtxo_ids: Vec<VtxoId>,
257
258 user_pub_nonces: Option<Vec<musig::PublicNonce>>,
263 user_sec_nonces: Option<Vec<musig::SecretNonce>>,
265 server_pub_nonces: Option<Vec<musig::PublicNonce>>,
267 server_partial_sigs: Option<Vec<musig::PartialSignature>>,
269 full_signatures: Option<Vec<schnorr::Signature>>,
271
272 _state: PhantomData<S>,
273}
274
275impl<S: state::BuilderState> CheckpointedArkoorBuilder<S> {
276 fn checkpoint_vtxo_at(
277 &self,
278 output_idx: usize,
279 checkpoint_sig: Option<schnorr::Signature>
280 ) -> Vtxo {
281 let output = &self.outputs[output_idx];
282 let (checkpoint_tx, checkpoint_txid, _tweak) = self.checkpoint_data.as_ref()
283 .expect("called checkpoint_vtxo_at in context without checkpoints");
284
285 Vtxo {
286 amount: output.amount,
287 policy: VtxoPolicy::new_checkpoint(self.input.user_pubkey()),
288 expiry_height: self.input.expiry_height,
289 server_pubkey: self.input.server_pubkey,
290 exit_delta: self.input.exit_delta,
291 point: OutPoint::new(*checkpoint_txid, output_idx as u32),
292 anchor_point: self.input.anchor_point,
293 genesis: self.input.genesis.clone().into_iter().chain([
294 GenesisItem {
295 transition: GenesisTransition::Arkoor {
296 policy: self.input.policy.clone(),
297 signature: checkpoint_sig,
298 },
299 output_idx: output_idx as u8,
300 other_outputs: checkpoint_tx.output
301 .iter().enumerate()
302 .filter_map(|(i, txout)| {
303 if i == (output_idx as usize) || txout.is_p2a_fee_anchor() {
304 None
305 } else {
306 Some(txout.clone())
307 }
308 })
309 .collect(),
310 },
311 ]).collect(),
312 }
313 }
314
315 fn vtxo_at(
316 &self,
317 output_idx: usize,
318 checkpoint_sig: Option<schnorr::Signature>,
319 arkoor_sig: Option<schnorr::Signature>,
320 ) -> Vtxo {
321 let output = &self.outputs[output_idx];
322
323 if let Some((checkpoint_tx, _txid, _tweak)) = &self.checkpoint_data {
324 let checkpoint_policy = VtxoPolicy::new_checkpoint(self.input.user_pubkey());
326
327 Vtxo {
328 amount: output.amount,
329 policy: output.policy.clone(),
330 expiry_height: self.input.expiry_height,
331 server_pubkey: self.input.server_pubkey,
332 exit_delta: self.input.exit_delta,
333 point: self.new_vtxo_ids[output_idx].utxo(),
334 anchor_point: self.input.anchor_point,
335 genesis: self.input.genesis.iter().cloned().chain([
336 GenesisItem {
337 transition: GenesisTransition::Arkoor {
338 policy: self.input.policy.clone(),
339 signature: checkpoint_sig,
340 },
341 output_idx: output_idx as u8,
342 other_outputs: checkpoint_tx.output
343 .iter().enumerate()
344 .filter_map(|(i, txout)| {
345 if i == (output_idx as usize) || txout.is_p2a_fee_anchor() {
346 None
347 } else {
348 Some(txout.clone())
349 }
350 })
351 .collect(),
352 },
353 GenesisItem {
354 transition: GenesisTransition::Arkoor {
355 policy: checkpoint_policy,
356 signature: arkoor_sig,
357 },
358 output_idx: 0,
359 other_outputs: vec![]
360 }
361 ]).collect(),
362 }
363 } else {
364 let arkoor_tx = &self.unsigned_arkoor_txs[0];
366
367 Vtxo {
368 amount: output.amount,
369 policy: output.policy.clone(),
370 expiry_height: self.input.expiry_height,
371 server_pubkey: self.input.server_pubkey,
372 exit_delta: self.input.exit_delta,
373 point: OutPoint::new(arkoor_tx.compute_txid(), output_idx as u32),
374 anchor_point: self.input.anchor_point,
375 genesis: self.input.genesis.iter().cloned().chain([
376 GenesisItem {
377 transition: GenesisTransition::Arkoor {
378 policy: self.input.policy.clone(),
379 signature: arkoor_sig,
380 },
381 output_idx: output_idx as u8,
382 other_outputs: arkoor_tx.output
383 .iter().enumerate()
384 .filter_map(|(idx, txout)| {
385 if idx == output_idx || txout.is_p2a_fee_anchor() {
386 None
387 } else {
388 Some(txout.clone())
389 }
390 })
391 .collect(),
392 }
393 ]).collect(),
394 }
395 }
396 }
397
398 fn construct_dust_vtxo_at(
406 &self,
407 dust_idx: usize,
408 pre_fanout_tx_sig: Option<schnorr::Signature>,
409 dust_fanout_tx_sig: Option<schnorr::Signature>,
410 exit_tx_sig: Option<schnorr::Signature>,
411 ) -> Vtxo {
412 let output = &self.dust_outputs[dust_idx];
413 let checkpoint_policy = VtxoPolicy::new_checkpoint(self.input.user_pubkey());
414
415 let fanout_tx = self.unsigned_dust_fanout_tx.as_ref()
416 .expect("construct_dust_vtxo_at called without dust isolation");
417 let exit_txs = self.unsigned_dust_exit_txs.as_ref()
418 .expect("construct_dust_vtxo_at called without dust isolation");
419
420 let dust_isolation_output_idx = self.outputs.len();
422
423 if let Some((checkpoint_tx, _txid, _tweak)) = &self.checkpoint_data {
424 Vtxo {
426 amount: output.amount,
427 policy: output.policy.clone(),
428 expiry_height: self.input.expiry_height,
429 server_pubkey: self.input.server_pubkey,
430 exit_delta: self.input.exit_delta,
431 point: OutPoint::new(exit_txs[dust_idx].compute_txid(), 0),
432 anchor_point: self.input.anchor_point,
433 genesis: self.input.genesis.iter().cloned().chain([
434 GenesisItem {
436 transition: GenesisTransition::Arkoor {
437 policy: self.input.policy.clone(),
438 signature: pre_fanout_tx_sig,
439 },
440 output_idx: dust_isolation_output_idx as u8,
441 other_outputs: checkpoint_tx.output
444 .iter().enumerate()
445 .filter_map(|(idx, txout)| {
446 if idx == dust_isolation_output_idx || txout.is_p2a_fee_anchor() {
447 None
448 } else {
449 Some(txout.clone())
450 }
451 })
452 .collect(),
453 },
454 GenesisItem {
456 transition: GenesisTransition::Arkoor {
457 policy: checkpoint_policy.clone(),
458 signature: dust_fanout_tx_sig,
459 },
460 output_idx: dust_idx as u8,
461 other_outputs: fanout_tx.output
464 .iter().enumerate()
465 .filter_map(|(idx, txout)| {
466 if idx == dust_idx || txout.is_p2a_fee_anchor() {
467 None
468 } else {
469 Some(txout.clone())
470 }
471 })
472 .collect(),
473 },
474 GenesisItem {
476 transition: GenesisTransition::Arkoor {
477 policy: checkpoint_policy,
478 signature: exit_tx_sig,
479 },
480 output_idx: 0,
481 other_outputs: vec![]
482 }
483 ]).collect(),
484 }
485 } else {
486 let arkoor_tx = &self.unsigned_arkoor_txs[0];
488
489 Vtxo {
490 amount: output.amount,
491 policy: output.policy.clone(),
492 expiry_height: self.input.expiry_height,
493 server_pubkey: self.input.server_pubkey,
494 exit_delta: self.input.exit_delta,
495 point: OutPoint::new(exit_txs[dust_idx].compute_txid(), 0),
496 anchor_point: self.input.anchor_point,
497 genesis: self.input.genesis.iter().cloned().chain([
498 GenesisItem {
500 transition: GenesisTransition::Arkoor {
501 policy: self.input.policy.clone(),
502 signature: pre_fanout_tx_sig, },
504 output_idx: dust_isolation_output_idx as u8,
505 other_outputs: arkoor_tx.output
506 .iter().enumerate()
507 .filter_map(|(idx, txout)| {
508 if idx == dust_isolation_output_idx || txout.is_p2a_fee_anchor() {
509 None
510 } else {
511 Some(txout.clone())
512 }
513 })
514 .collect(),
515 },
516 GenesisItem {
518 transition: GenesisTransition::Arkoor {
519 policy: checkpoint_policy.clone(),
520 signature: dust_fanout_tx_sig,
521 },
522 output_idx: dust_idx as u8,
523 other_outputs: fanout_tx.output
524 .iter().enumerate()
525 .filter_map(|(idx, txout)| {
526 if idx == dust_idx || txout.is_p2a_fee_anchor() {
527 None
528 } else {
529 Some(txout.clone())
530 }
531 })
532 .collect(),
533 },
534 GenesisItem {
536 transition: GenesisTransition::Arkoor {
537 policy: checkpoint_policy,
538 signature: exit_tx_sig,
539 },
540 output_idx: 0,
541 other_outputs: vec![]
542 }
543 ]).collect(),
544 }
545 }
546 }
547
548 fn nb_sigs(&self) -> usize {
549 let base = if self.checkpoint_data.is_some() {
550 1 + self.outputs.len() } else {
552 1 };
554
555 if self.unsigned_dust_fanout_tx.is_some() {
556 base + 1 + self.dust_outputs.len()
557 } else {
558 base
559 }
560 }
561
562 fn nb_outputs(&self) -> usize {
564 self.outputs.len()
565 }
566
567 pub fn build_unsigned_vtxos<'a>(&'a self) -> impl Iterator<Item = Vtxo> + 'a {
568 (0..self.nb_outputs()).map(|i| self.vtxo_at(i, None, None))
569 }
570
571 pub fn build_unsigned_dust_vtxos<'a>(&'a self) -> impl Iterator<Item = Vtxo> + 'a {
573 (0..self.dust_outputs.len()).map(|i| self.construct_dust_vtxo_at(i, None, None, None))
574 }
575
576 pub fn build_unsigned_checkpoint_vtxos<'a>(&'a self) -> impl Iterator<Item = Vtxo> + 'a {
577 (0..self.nb_outputs()).map(|i| self.checkpoint_vtxo_at(i, None))
578 }
579
580 pub fn spend_info(&self) -> Vec<(VtxoId, Txid)> {
582 let mut ret = Vec::with_capacity(1 + self.nb_outputs());
583
584 if let Some((_tx, checkpoint_txid, _tweak)) = &self.checkpoint_data {
585 ret.push((self.input.id(), *checkpoint_txid));
587
588 for idx in 0..self.nb_outputs() {
590 ret.push((
591 VtxoId::from(OutPoint::new(*checkpoint_txid, idx as u32)),
592 self.unsigned_arkoor_txs[idx].compute_txid()
593 ));
594 }
595
596 if let (Some(fanout_tx), Some(exit_txs))
598 = (&self.unsigned_dust_fanout_tx, &self.unsigned_dust_exit_txs)
599 {
600 let fanout_txid = fanout_tx.compute_txid();
601
602 let dust_output_idx = self.outputs.len() as u32;
604 ret.push((
605 VtxoId::from(OutPoint::new(*checkpoint_txid, dust_output_idx)),
606 fanout_txid
607 ));
608
609 for (idx, exit_tx) in exit_txs.iter().enumerate() {
611 ret.push((
612 VtxoId::from(OutPoint::new(fanout_txid, idx as u32)),
613 exit_tx.compute_txid()
614 ));
615 }
616 }
617 } else {
618 let arkoor_txid = self.unsigned_arkoor_txs[0].compute_txid();
619
620 ret.push((self.input.id(), arkoor_txid));
622
623 if let (Some(fanout_tx), Some(exit_txs))
625 = (&self.unsigned_dust_fanout_tx, &self.unsigned_dust_exit_txs)
626 {
627 let fanout_txid = fanout_tx.compute_txid();
628
629 let dust_output_idx = self.outputs.len() as u32;
631 ret.push((
632 VtxoId::from(OutPoint::new(arkoor_txid, dust_output_idx)),
633 fanout_txid
634 ));
635
636 for (idx, exit_tx) in exit_txs.iter().enumerate() {
638 ret.push((
639 VtxoId::from(OutPoint::new(fanout_txid, idx as u32)),
640 exit_tx.compute_txid()
641 ));
642 }
643 }
644 }
645
646 ret
647 }
648
649 pub fn checkpoint_spend_info(&self) -> Vec<(Vtxo, Txid)> {
654 if self.checkpoint_data.is_none() {
655 return vec![];
656 }
657
658 let mut result = Vec::with_capacity(self.nb_outputs());
659
660 for idx in 0..self.nb_outputs() {
661 let vtxo = self.checkpoint_vtxo_at(idx, None);
662 result.push((vtxo, self.new_vtxo_ids[idx].utxo().txid))
663 }
664
665 result
666 }
667
668 fn taptweak_at(&self, idx: usize) -> TapTweakHash {
669 if let Some((_tx, _txid, checkpoint_tweak)) = &self.checkpoint_data {
670 if idx == 0 {
671 *checkpoint_tweak
672 } else {
673 self.arkoor_taptweak
674 }
675 } else {
676 self.arkoor_taptweak
677 }
678 }
679
680 fn user_pubkey(&self) -> PublicKey {
681 self.input.user_pubkey()
682 }
683
684 fn server_pubkey(&self) -> PublicKey {
685 self.input.server_pubkey()
686 }
687
688 fn construct_unsigned_checkpoint_tx(
691 input: &Vtxo,
692 outputs: &[VtxoRequest],
693 dust_isolation_amount: Option<Amount>,
694 ) -> Transaction {
695 let output_policy = VtxoPolicy::new_checkpoint(input.user_pubkey());
697 let checkpoint_spk = output_policy.script_pubkey(input.server_pubkey(), input.exit_delta(), input.expiry_height());
698
699 Transaction {
700 version: bitcoin::transaction::Version(3),
701 lock_time: bitcoin::absolute::LockTime::ZERO,
702 input: vec![TxIn {
703 previous_output: input.point(),
704 script_sig: ScriptBuf::new(),
705 sequence: Sequence::ZERO,
706 witness: Witness::new(),
707 }],
708 output: outputs.iter().map(|o| {
709 TxOut {
710 value: o.amount,
711 script_pubkey: checkpoint_spk.clone(),
712 }
713 })
714 .chain(dust_isolation_amount.map(|amt| {
716 TxOut {
717 value: amt,
718 script_pubkey: checkpoint_spk.clone(),
719 }
720 }))
721 .chain([fee::fee_anchor()]).collect()
722 }
723 }
724
725 fn construct_unsigned_arkoor_txs(
726 input: &Vtxo,
727 outputs: &[VtxoRequest],
728 checkpoint_txid: Option<Txid>,
729 dust_isolation_amount: Option<Amount>,
730 ) -> Vec<Transaction> {
731 if let Some(checkpoint_txid) = checkpoint_txid {
732 let mut arkoor_txs = Vec::with_capacity(outputs.len());
734
735 for (vout, output) in outputs.iter().enumerate() {
736 let transaction = Transaction {
737 version: bitcoin::transaction::Version(3),
738 lock_time: bitcoin::absolute::LockTime::ZERO,
739 input: vec![TxIn {
740 previous_output: OutPoint::new(checkpoint_txid, vout as u32),
741 script_sig: ScriptBuf::new(),
742 sequence: Sequence::ZERO,
743 witness: Witness::new(),
744 }],
745 output: vec![
746 output.policy.txout(output.amount, input.server_pubkey(), input.exit_delta(), input.expiry_height()),
747 fee::fee_anchor(),
748 ]
749 };
750 arkoor_txs.push(transaction);
751 }
752
753 arkoor_txs
754 } else {
755 let checkpoint_policy = VtxoPolicy::new_checkpoint(input.user_pubkey());
757 let checkpoint_spk = checkpoint_policy.script_pubkey(
758 input.server_pubkey(),
759 input.exit_delta(),
760 input.expiry_height()
761 );
762
763 let transaction = Transaction {
764 version: bitcoin::transaction::Version(3),
765 lock_time: bitcoin::absolute::LockTime::ZERO,
766 input: vec![TxIn {
767 previous_output: input.point(),
768 script_sig: ScriptBuf::new(),
769 sequence: Sequence::ZERO,
770 witness: Witness::new(),
771 }],
772 output: outputs.iter()
773 .map(|o| o.policy.txout(o.amount, input.server_pubkey(), input.exit_delta(), input.expiry_height()))
774 .chain(dust_isolation_amount.map(|amt| TxOut {
776 value: amt,
777 script_pubkey: checkpoint_spk.clone(),
778 }))
779 .chain([fee::fee_anchor()])
780 .collect()
781 };
782 vec![transaction]
783 }
784 }
785
786 fn construct_unsigned_dust_fanout_tx(
794 input: &Vtxo,
795 dust_outputs: &[VtxoRequest],
796 parent_txid: Txid, dust_isolation_output_vout: u32, ) -> Transaction {
799 let output_policy = VtxoPolicy::new_checkpoint(input.user_pubkey());
801 let checkpoint_spk = output_policy.script_pubkey(input.server_pubkey(), input.exit_delta(), input.expiry_height());
802
803 let mut tx_outputs: Vec<TxOut> = dust_outputs.iter().map(|o| {
804 TxOut {
805 value: o.amount,
806 script_pubkey: checkpoint_spk.clone(),
807 }
808 }).collect();
809
810 tx_outputs.push(fee::fee_anchor());
812
813 Transaction {
814 version: bitcoin::transaction::Version(3),
815 lock_time: bitcoin::absolute::LockTime::ZERO,
816 input: vec![TxIn {
817 previous_output: OutPoint::new(parent_txid, dust_isolation_output_vout),
818 script_sig: ScriptBuf::new(),
819 sequence: Sequence::ZERO,
820 witness: Witness::new(),
821 }],
822 output: tx_outputs,
823 }
824 }
825
826 fn construct_unsigned_dust_exit_txs(
832 input: &Vtxo,
833 dust_outputs: &[VtxoRequest],
834 dust_fanout_tx: &Transaction,
835 ) -> Vec<Transaction> {
836 let fanout_txid = dust_fanout_tx.compute_txid();
837
838 dust_outputs.iter().enumerate().map(|(vout, output)| {
839 Transaction {
840 version: bitcoin::transaction::Version(3),
841 lock_time: bitcoin::absolute::LockTime::ZERO,
842 input: vec![TxIn {
843 previous_output: OutPoint::new(fanout_txid, vout as u32),
844 script_sig: ScriptBuf::new(),
845 sequence: Sequence::ZERO,
846 witness: Witness::new(),
847 }],
848 output: vec![
849 output.policy.txout(
851 output.amount,
852 input.server_pubkey(),
853 input.exit_delta(),
854 input.expiry_height(),
855 ),
856 fee::fee_anchor(),
857 ]
858 }
859 }).collect()
860 }
861
862 fn validate_amounts(
863 input: &Vtxo,
864 outputs: &[VtxoRequest],
865 isolation_outputs: &[VtxoRequest],
866 ) -> Result<(), ArkoorConstructionError> {
867 let input_amount = input.amount();
872 let output_amount = outputs.iter().chain(isolation_outputs.iter())
873 .map(|o| o.amount).sum::<Amount>();
874
875 if input_amount != output_amount {
876 return Err(ArkoorConstructionError::Unbalanced {
877 input: input_amount,
878 output: output_amount,
879 })
880 }
881
882 if !isolation_outputs.is_empty() {
886 if outputs.iter().any(|o| o.amount < P2TR_DUST) {
887 return Err(ArkoorConstructionError::Dust)
888 }
889
890 let dust_sum: Amount = isolation_outputs.iter().map(|o| o.amount).sum();
891 if dust_sum < P2TR_DUST {
892 return Err(ArkoorConstructionError::Dust)
893 }
894 } else {
895 let nb_dust = outputs.iter().filter(|o| o.amount < P2TR_DUST).count();
897 if !(nb_dust == 0 || nb_dust == outputs.len()) {
898 return Err(ArkoorConstructionError::Dust)
899 }
900 }
901
902 Ok(())
903 }
904
905
906 fn to_state<S2: state::BuilderState>(self) -> CheckpointedArkoorBuilder<S2> {
907 CheckpointedArkoorBuilder {
908 input: self.input,
909 outputs: self.outputs,
910 dust_outputs: self.dust_outputs,
911 checkpoint_data: self.checkpoint_data,
912 unsigned_arkoor_txs: self.unsigned_arkoor_txs,
913 unsigned_dust_fanout_tx: self.unsigned_dust_fanout_tx,
914 unsigned_dust_exit_txs: self.unsigned_dust_exit_txs,
915 new_vtxo_ids: self.new_vtxo_ids,
916 sighashes: self.sighashes,
917 arkoor_taptweak: self.arkoor_taptweak,
918 user_pub_nonces: self.user_pub_nonces,
919 user_sec_nonces: self.user_sec_nonces,
920 server_pub_nonces: self.server_pub_nonces,
921 server_partial_sigs: self.server_partial_sigs,
922 full_signatures: self.full_signatures,
923 _state: PhantomData,
924 }
925 }
926}
927
928impl CheckpointedArkoorBuilder<state::Initial> {
929 pub fn new_with_checkpoint(
931 input: Vtxo,
932 outputs: Vec<VtxoRequest>,
933 dust_outputs: Vec<VtxoRequest>,
934 ) -> Result<Self, ArkoorConstructionError> {
935 Self::new(input, outputs, dust_outputs, true)
936 }
937
938 pub fn new_without_checkpoint(
940 input: Vtxo,
941 outputs: Vec<VtxoRequest>,
942 dust_outputs: Vec<VtxoRequest>,
943 ) -> Result<Self, ArkoorConstructionError> {
944 Self::new(input, outputs, dust_outputs, false)
945 }
946
947 fn new(
948 input: Vtxo,
949 mut outputs: Vec<VtxoRequest>,
950 mut dust_outputs: Vec<VtxoRequest>,
951 use_checkpoint: bool,
952 ) -> Result<Self, ArkoorConstructionError> {
953 if outputs.is_empty() && !dust_outputs.is_empty() {
955 std::mem::swap(&mut outputs, &mut dust_outputs);
956 }
957
958 Self::validate_amounts(&input, &outputs, &dust_outputs)?;
960
961 let combined_dust_amount = if !dust_outputs.is_empty() {
963 Some(dust_outputs.iter().map(|o| o.amount).sum())
964 } else {
965 None
966 };
967
968 let unsigned_checkpoint_tx = if use_checkpoint {
970 let tx = Self::construct_unsigned_checkpoint_tx(
971 &input,
972 &outputs,
973 combined_dust_amount,
974 );
975 let txid = tx.compute_txid();
976 let taptweak = input.output_taproot().tap_tweak();
977 Some((tx, txid, taptweak))
978 } else {
979 None
980 };
981
982 let unsigned_arkoor_txs = Self::construct_unsigned_arkoor_txs(
984 &input,
985 &outputs,
986 unsigned_checkpoint_tx.as_ref().map(|t| t.1),
987 combined_dust_amount,
988 );
989
990 let (unsigned_dust_fanout_tx, unsigned_dust_exit_txs) = if !dust_outputs.is_empty() {
992 let dust_isolation_output_vout = outputs.len() as u32;
995
996 let parent_txid = if let Some((_tx, txid, _tweak)) = &unsigned_checkpoint_tx {
997 *txid
998 } else {
999 unsigned_arkoor_txs[0].compute_txid()
1000 };
1001
1002 let fanout_tx = Self::construct_unsigned_dust_fanout_tx(
1003 &input,
1004 &dust_outputs,
1005 parent_txid,
1006 dust_isolation_output_vout,
1007 );
1008 let exit_txs = Self::construct_unsigned_dust_exit_txs(
1009 &input,
1010 &dust_outputs,
1011 &fanout_tx,
1012 );
1013 (Some(fanout_tx), Some(exit_txs))
1014 } else {
1015 (None, None)
1016 };
1017
1018 let new_vtxo_ids = unsigned_arkoor_txs.iter()
1020 .map(|tx| OutPoint::new(tx.compute_txid(), 0))
1021 .map(|outpoint| VtxoId::from(outpoint))
1022 .collect();
1023
1024 let mut sighashes = Vec::new();
1026
1027 if let Some((checkpoint_tx, _txid, _tweak)) = &unsigned_checkpoint_tx {
1028 sighashes.push(arkoor_sighash(&input.txout(), checkpoint_tx));
1030
1031 for vout in 0..outputs.len() {
1033 let prevout = checkpoint_tx.output[vout].clone();
1034 sighashes.push(arkoor_sighash(&prevout, &unsigned_arkoor_txs[vout]));
1035 }
1036 } else {
1037 sighashes.push(arkoor_sighash(&input.txout(), &unsigned_arkoor_txs[0]));
1039 }
1040
1041 if let Some(ref tx) = unsigned_dust_fanout_tx {
1043 let dust_output_vout = outputs.len(); let prevout = if let Some((checkpoint_tx, _txid, _tweak)) = &unsigned_checkpoint_tx {
1045 checkpoint_tx.output[dust_output_vout].clone()
1046 } else {
1047 unsigned_arkoor_txs[0].output[dust_output_vout].clone()
1049 };
1050 sighashes.push(arkoor_sighash(&prevout, tx));
1051 }
1052
1053 if let (Some(fanout_tx), Some(exit_txs))
1055 = (&unsigned_dust_fanout_tx, &unsigned_dust_exit_txs)
1056 {
1057 for (vout, exit_tx) in exit_txs.iter().enumerate() {
1058 let prevout = fanout_tx.output[vout].clone();
1059 sighashes.push(arkoor_sighash(&prevout, exit_tx));
1060 }
1061 }
1062
1063 let policy = VtxoPolicy::new_checkpoint(input.user_pubkey());
1065 let arkoor_taptweak = if use_checkpoint {
1066 policy.taproot(
1067 input.server_pubkey(),
1068 input.exit_delta(),
1069 input.expiry_height(),
1070 ).tap_tweak()
1071 } else {
1072 input.output_taproot().tap_tweak()
1074 };
1075
1076 Ok(Self {
1077 input: input,
1078 outputs: outputs,
1079 dust_outputs: dust_outputs,
1080 sighashes: sighashes,
1081 arkoor_taptweak: arkoor_taptweak,
1082 checkpoint_data: unsigned_checkpoint_tx,
1083 unsigned_arkoor_txs: unsigned_arkoor_txs,
1084 unsigned_dust_fanout_tx: unsigned_dust_fanout_tx,
1085 unsigned_dust_exit_txs: unsigned_dust_exit_txs,
1086 new_vtxo_ids: new_vtxo_ids,
1087 user_pub_nonces: None,
1088 user_sec_nonces: None,
1089 server_pub_nonces: None,
1090 server_partial_sigs: None,
1091 full_signatures: None,
1092 _state: PhantomData,
1093 })
1094 }
1095
1096 pub fn generate_user_nonces(mut self, user_keypair: Keypair) -> CheckpointedArkoorBuilder<state::UserGeneratedNonces> {
1099 let mut user_pub_nonces = Vec::with_capacity(self.nb_sigs());
1100 let mut user_sec_nonces = Vec::with_capacity(self.nb_sigs());
1101
1102 for idx in 0..self.nb_sigs() {
1103 let sighash = &self.sighashes[idx].to_byte_array();
1104 let (sec_nonce, pub_nonce) = musig::nonce_pair_with_msg(&user_keypair, sighash);
1105
1106 user_pub_nonces.push(pub_nonce);
1107 user_sec_nonces.push(sec_nonce);
1108 }
1109
1110 self.user_pub_nonces = Some(user_pub_nonces);
1111 self.user_sec_nonces = Some(user_sec_nonces);
1112
1113 self.to_state::<state::UserGeneratedNonces>()
1114 }
1115
1116 fn set_user_pub_nonces(mut self, user_pub_nonces: Vec<musig::PublicNonce>) -> Result<CheckpointedArkoorBuilder<state::ServerCanCosign>, ArkoorSigningError> {
1122 if user_pub_nonces.len() != self.nb_sigs() {
1123 return Err(ArkoorSigningError::InvalidNbUserNonces {
1124 expected: self.nb_sigs(),
1125 got: user_pub_nonces.len()
1126 })
1127 }
1128
1129 self.user_pub_nonces = Some(user_pub_nonces);
1130 Ok(self.to_state::<state::ServerCanCosign>())
1131 }
1132}
1133
1134impl<'a> CheckpointedArkoorBuilder<state::ServerCanCosign> {
1135
1136 pub fn from_cosign_request(
1137 cosign_request: CosignRequest<Vtxo>,
1138 ) -> Result<CheckpointedArkoorBuilder<state::ServerCanCosign>, ArkoorSigningError> {
1139 CheckpointedArkoorBuilder::new(
1140 cosign_request.input,
1141 cosign_request.outputs,
1142 cosign_request.dust_outputs,
1143 cosign_request.use_checkpoint,
1144 )
1145 .map_err(ArkoorSigningError::ArkoorConstructionError)?
1146 .set_user_pub_nonces(cosign_request.user_pub_nonces.clone())
1147 }
1148
1149 pub fn server_cosign(mut self, server_keypair: Keypair) -> Result<CheckpointedArkoorBuilder<state::ServerSigned>, ArkoorSigningError> {
1150 if server_keypair.public_key() != self.input.server_pubkey() {
1152 return Err(ArkoorSigningError::IncorrectKey {
1153 expected: self.input.server_pubkey(),
1154 got: server_keypair.public_key(),
1155 });
1156 }
1157
1158 let mut server_pub_nonces = Vec::with_capacity(self.outputs.len() + 1);
1159 let mut server_partial_sigs = Vec::with_capacity(self.outputs.len() + 1);
1160
1161 for idx in 0..self.nb_sigs() {
1162 let (server_pub_nonce, server_partial_sig) = musig::deterministic_partial_sign(
1163 &server_keypair,
1164 [self.input.user_pubkey()],
1165 &[&self.user_pub_nonces.as_ref().expect("state-invariant")[idx]],
1166 self.sighashes[idx].to_byte_array(),
1167 Some(self.taptweak_at(idx).to_byte_array()),
1168 );
1169
1170 server_pub_nonces.push(server_pub_nonce);
1171 server_partial_sigs.push(server_partial_sig);
1172 };
1173
1174 self.server_pub_nonces = Some(server_pub_nonces);
1175 self.server_partial_sigs = Some(server_partial_sigs);
1176 Ok(self.to_state::<state::ServerSigned>())
1177 }
1178
1179}
1180
1181impl CheckpointedArkoorBuilder<state::ServerSigned> {
1182
1183 pub fn user_pub_nonces(&self) -> Vec<musig::PublicNonce> {
1184 self.user_pub_nonces.as_ref().expect("state invariant").clone()
1185 }
1186
1187 pub fn server_partial_signatures(&self) -> Vec<musig::PartialSignature> {
1188 self.server_partial_sigs.as_ref().expect("state invariant").clone()
1189 }
1190
1191 pub fn cosign_response(&self) -> CosignResponse {
1192 CosignResponse {
1193 server_pub_nonces: self.server_pub_nonces.as_ref().expect("state invariant").clone(),
1194 server_partial_sigs: self.server_partial_sigs.as_ref().expect("state invariant").clone(),
1195 }
1196 }
1197}
1198
1199impl CheckpointedArkoorBuilder<state::UserGeneratedNonces> {
1200
1201 pub fn user_pub_nonces(&self) -> &[PublicNonce] {
1202 self.user_pub_nonces.as_ref().expect("State invariant")
1203 }
1204
1205 pub fn cosign_request(&self) -> CosignRequest<Vtxo> {
1206 CosignRequest {
1207 user_pub_nonces: self.user_pub_nonces().to_vec(),
1208 input: self.input.clone(),
1209 outputs: self.outputs.clone(),
1210 dust_outputs: self.dust_outputs.clone(),
1211 use_checkpoint: self.checkpoint_data.is_some(),
1212 }
1213 }
1214
1215 fn validate_server_cosign_response(
1216 &self,
1217 data: &CosignResponse,
1218 ) -> Result<(), ArkoorSigningError> {
1219
1220 if data.server_pub_nonces.len() != self.nb_sigs() {
1222 return Err(ArkoorSigningError::InvalidNbServerNonces {
1223 expected: self.nb_sigs(),
1224 got: data.server_pub_nonces.len(),
1225 });
1226 }
1227
1228 if data.server_partial_sigs.len() != self.nb_sigs() {
1229 return Err(ArkoorSigningError::InvalidNbServerPartialSigs {
1230 expected: self.nb_sigs(),
1231 got: data.server_partial_sigs.len(),
1232 })
1233 }
1234
1235 for idx in 0..self.nb_sigs() {
1237 let is_valid_sig = scripts::verify_partial_sig(
1238 self.sighashes[idx],
1239 self.taptweak_at(idx),
1240 (self.input.server_pubkey(), &data.server_pub_nonces[idx]),
1241 (self.input.user_pubkey(), &self.user_pub_nonces()[idx]),
1242 &data.server_partial_sigs[idx]
1243 );
1244
1245 if !is_valid_sig {
1246 return Err(ArkoorSigningError::InvalidPartialSignature {
1247 index: idx,
1248 });
1249 }
1250 }
1251 Ok(())
1252 }
1253
1254 pub fn user_cosign(
1255 mut self,
1256 user_keypair: &Keypair,
1257 server_cosign_data: &CosignResponse,
1258 ) -> Result<CheckpointedArkoorBuilder<state::UserSigned>, ArkoorSigningError> {
1259 if user_keypair.public_key() != self.input.user_pubkey() {
1261 return Err(ArkoorSigningError::IncorrectKey {
1262 expected: self.input.user_pubkey(),
1263 got: user_keypair.public_key(),
1264 });
1265 }
1266
1267 self.validate_server_cosign_response(&server_cosign_data)?;
1269
1270 let mut sigs = Vec::with_capacity(self.nb_sigs());
1271
1272 let user_sec_nonces = self.user_sec_nonces.take().expect("state invariant");
1275
1276 for (idx, user_sec_nonce) in user_sec_nonces.into_iter().enumerate() {
1277 let user_pub_nonce = self.user_pub_nonces()[idx];
1278 let server_pub_nonce = server_cosign_data.server_pub_nonces[idx];
1279 let agg_nonce = musig::nonce_agg(&[&user_pub_nonce, &server_pub_nonce]);
1280
1281 let (_partial, maybe_sig) = musig::partial_sign(
1282 [self.user_pubkey(), self.server_pubkey()],
1283 agg_nonce,
1284 &user_keypair,
1285 user_sec_nonce,
1286 self.sighashes[idx].to_byte_array(),
1287 Some(self.taptweak_at(idx).to_byte_array()),
1288 Some(&[&server_cosign_data.server_partial_sigs[idx]])
1289 );
1290
1291 let sig = maybe_sig.expect("The full signature exists. The server did sign first");
1292 sigs.push(sig);
1293 }
1294
1295
1296 self.full_signatures = Some(sigs);
1297
1298 Ok(self.to_state::<state::UserSigned>())
1299 }
1300}
1301
1302
1303impl<'a> CheckpointedArkoorBuilder<state::UserSigned> {
1304
1305 pub fn build_signed_vtxos(&self) -> Vec<Vtxo> {
1306 let sigs = self.full_signatures.as_ref().expect("state invariant");
1307 let mut ret = Vec::with_capacity(self.outputs.len() + self.dust_outputs.len());
1308
1309 if self.checkpoint_data.is_some() {
1310 let checkpoint_sig = sigs[0];
1311
1312 for i in 0..self.outputs.len() {
1314 let arkoor_sig = sigs[1 + i];
1315 ret.push(self.vtxo_at(i, Some(checkpoint_sig), Some(arkoor_sig)));
1316 }
1317
1318 if self.unsigned_dust_fanout_tx.is_some() {
1320 let m = self.outputs.len();
1321 let dust_fanout_tx_sig = sigs[1 + m];
1322
1323 for i in 0..self.dust_outputs.len() {
1324 let exit_tx_sig = sigs[2 + m + i];
1325 ret.push(self.construct_dust_vtxo_at(
1326 i,
1327 Some(checkpoint_sig),
1328 Some(dust_fanout_tx_sig),
1329 Some(exit_tx_sig),
1330 ));
1331 }
1332 }
1333 } else {
1334 let arkoor_sig = sigs[0];
1336
1337 for i in 0..self.outputs.len() {
1339 ret.push(self.vtxo_at(i, None, Some(arkoor_sig)));
1340 }
1341
1342 if self.unsigned_dust_fanout_tx.is_some() {
1344 let dust_fanout_tx_sig = sigs[1];
1345
1346 for i in 0..self.dust_outputs.len() {
1347 let exit_tx_sig = sigs[2 + i];
1348 ret.push(self.construct_dust_vtxo_at(
1349 i,
1350 Some(arkoor_sig), Some(dust_fanout_tx_sig),
1352 Some(exit_tx_sig),
1353 ));
1354 }
1355 }
1356 }
1357
1358 ret
1359 }
1360}
1361
1362
1363#[cfg(test)]
1364mod test {
1365 use super::*;
1366
1367 use bitcoin::Amount;
1368 use bitcoin::secp256k1::Keypair;
1369 use bitcoin::secp256k1::rand;
1370
1371 use crate::SECP;
1372 use crate::VtxoRequest;
1373 use crate::test::dummy::DummyTestVtxoSpec;
1374
1375
1376 #[test]
1377 fn build_checkpointed_arkoor() {
1378 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1379 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1380 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1381
1382 println!("Alice keypair: {}", alice_keypair.public_key());
1383 println!("Bob keypair: {}", bob_keypair.public_key());
1384 println!("Server keypair: {}", server_keypair.public_key());
1385 println!("-----------------------------------------------");
1386
1387 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1388 amount: Amount::from_sat(100_000),
1389 expiry_height: 1000,
1390 exit_delta : 128,
1391 user_keypair: alice_keypair.clone(),
1392 server_keypair: server_keypair.clone()
1393 }.build();
1394
1395 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1397
1398 let vtxo_request = vec![
1399 VtxoRequest {
1400 amount: Amount::from_sat(96_000),
1401 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1402 },
1403 VtxoRequest {
1404 amount: Amount::from_sat(4_000),
1405 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1406 }
1407 ];
1408
1409 let user_builder = CheckpointedArkoorBuilder::new_with_checkpoint(
1411 alice_vtxo.clone(),
1412 vtxo_request.clone(),
1413 vec![], ).expect("Valid arkoor request");
1415
1416 let _unsigned_vtxos = user_builder.build_unsigned_vtxos().collect::<Vec<_>>();
1420
1421
1422 let user_builder =user_builder.generate_user_nonces(alice_keypair);
1424 let cosign_request = user_builder.cosign_request();
1425
1426 let server_builder = CheckpointedArkoorBuilder::from_cosign_request(cosign_request).expect("Invalid cosign request")
1428 .server_cosign(server_keypair).expect("Incorrect key");
1429
1430 let cosign_data = server_builder.cosign_response();
1431
1432 let vtxos = user_builder
1434 .user_cosign(&alice_keypair, &cosign_data)
1435 .expect("Valid cosign data and correct key")
1436 .build_signed_vtxos();
1437
1438 for vtxo in vtxos.into_iter() {
1439 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1441
1442 let mut prev_tx = funding_tx.clone();
1444 for tx in vtxo.transactions().map(|item| item.tx) {
1445 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1446 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1447 crate::test::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1448 prev_tx = tx;
1449 }
1450 }
1451
1452 }
1453
1454 #[test]
1455 fn build_checkpointed_arkoor_with_dust_isolation() {
1456 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1459 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1460 let charlie_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1461 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1462
1463 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1464 amount: Amount::from_sat(100_000),
1465 expiry_height: 1000,
1466 exit_delta : 128,
1467 user_keypair: alice_keypair.clone(),
1468 server_keypair: server_keypair.clone()
1469 }.build();
1470
1471 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1473
1474 let outputs = vec![
1476 VtxoRequest {
1477 amount: Amount::from_sat(99_600),
1478 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1479 },
1480 ];
1481
1482 let dust_outputs = vec![
1484 VtxoRequest {
1485 amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(charlie_keypair.public_key())
1487 },
1488 VtxoRequest {
1489 amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1491 }
1492 ];
1493
1494 let user_builder = CheckpointedArkoorBuilder::new_with_checkpoint(
1496 alice_vtxo.clone(),
1497 outputs.clone(),
1498 dust_outputs.clone(),
1499 ).expect("Valid arkoor request with dust isolation");
1500
1501 assert!(user_builder.unsigned_dust_fanout_tx.is_some(), "Dust isolation should be active");
1503 assert!(user_builder.unsigned_dust_exit_txs.is_some(), "Dust exit txs should be present");
1504 assert_eq!(user_builder.unsigned_dust_exit_txs.as_ref().unwrap().len(), 2);
1505
1506 assert_eq!(user_builder.nb_sigs(), 5);
1508
1509 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1511 let cosign_request = user_builder.cosign_request();
1512
1513 let server_builder = CheckpointedArkoorBuilder::from_cosign_request(cosign_request).expect("Invalid cosign request")
1515 .server_cosign(server_keypair).expect("Incorrect key");
1516
1517 let cosign_data = server_builder.cosign_response();
1518
1519 let vtxos = user_builder
1521 .user_cosign(&alice_keypair, &cosign_data)
1522 .expect("Valid cosign data and correct key")
1523 .build_signed_vtxos();
1524
1525 assert_eq!(vtxos.len(), 3);
1527
1528 for vtxo in vtxos.into_iter() {
1529 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1531
1532 let mut prev_tx = funding_tx.clone();
1534 for tx in vtxo.transactions().map(|item| item.tx) {
1535 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1536 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1537 crate::test::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1538 prev_tx = tx;
1539 }
1540 }
1541 }
1542
1543 #[test]
1544 fn build_checkpointed_arkoor_outputs_must_be_above_dust_if_mixed() {
1545 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1547 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1548 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1549
1550 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1551 amount: Amount::from_sat(1000),
1552 expiry_height: 1000,
1553 exit_delta : 128,
1554 user_keypair: alice_keypair.clone(),
1555 server_keypair: server_keypair.clone()
1556 }.build();
1557
1558 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1559
1560 CheckpointedArkoorBuilder::new_with_checkpoint(
1562 alice_vtxo.clone(),
1563 vec![
1564 VtxoRequest {
1565 amount: Amount::from_sat(100), policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1567 }; 10
1568 ],
1569 vec![],
1570 ).unwrap();
1571
1572 CheckpointedArkoorBuilder::new_with_checkpoint(
1574 alice_vtxo.clone(),
1575 vec![],
1576 vec![
1577 VtxoRequest {
1578 amount: Amount::from_sat(100), policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1580 }; 10
1581 ],
1582 ).unwrap();
1583
1584 CheckpointedArkoorBuilder::new_with_checkpoint(
1586 alice_vtxo.clone(),
1587 vec![
1588 VtxoRequest {
1589 amount: Amount::from_sat(330), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1591 }; 2
1592 ],
1593 vec![
1594 VtxoRequest {
1595 amount: Amount::from_sat(170),
1596 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1597 }; 2
1598 ],
1599 ).unwrap();
1600
1601 let res_mixed = CheckpointedArkoorBuilder::new_with_checkpoint(
1603 alice_vtxo.clone(),
1604 vec![
1605 VtxoRequest {
1606 amount: Amount::from_sat(500),
1607 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1608 },
1609 VtxoRequest {
1610 amount: Amount::from_sat(300),
1611 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1612 }
1613 ],
1614 vec![
1615 VtxoRequest {
1616 amount: Amount::from_sat(100),
1617 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1618 }; 2
1619 ],
1620 );
1621 match res_mixed {
1622 Err(ArkoorConstructionError::Dust) => {},
1623 _ => panic!("Expected Dust error"),
1624 }
1625 }
1626
1627 #[test]
1628 fn build_checkpointed_arkoor_dust_sum_too_small() {
1629 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1631 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1632 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1633
1634 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1635 amount: Amount::from_sat(100_000),
1636 expiry_height: 1000,
1637 exit_delta : 128,
1638 user_keypair: alice_keypair.clone(),
1639 server_keypair: server_keypair.clone()
1640 }.build();
1641
1642 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1643
1644 let outputs = vec![
1646 VtxoRequest {
1647 amount: Amount::from_sat(99_900),
1648 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1649 },
1650 ];
1651
1652 let dust_outputs = vec![
1654 VtxoRequest {
1655 amount: Amount::from_sat(50),
1656 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1657 },
1658 VtxoRequest {
1659 amount: Amount::from_sat(50),
1660 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1661 }
1662 ];
1663
1664 let result = CheckpointedArkoorBuilder::new_with_checkpoint(
1666 alice_vtxo.clone(),
1667 outputs.clone(),
1668 dust_outputs.clone(),
1669 );
1670
1671 match result {
1672 Err(ArkoorConstructionError::Dust) => {},
1673 _ => panic!("Expected Dust error"),
1674 }
1675 }
1676
1677 #[test]
1678 fn spend_dust_vtxo() {
1679 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1681 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1682 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1683
1684 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1686 amount: Amount::from_sat(200),
1687 expiry_height: 1000,
1688 exit_delta: 128,
1689 user_keypair: alice_keypair.clone(),
1690 server_keypair: server_keypair.clone()
1691 }.build();
1692
1693 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1694
1695 let outputs = vec![];
1698 let dust_outputs = vec![
1699 VtxoRequest {
1700 amount: Amount::from_sat(100),
1701 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1702 },
1703 VtxoRequest {
1704 amount: Amount::from_sat(100),
1705 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1706 }
1707 ];
1708
1709 let user_builder = CheckpointedArkoorBuilder::new_with_checkpoint(
1710 alice_vtxo.clone(),
1711 outputs,
1712 dust_outputs,
1713 ).expect("Valid arkoor request for all-dust case");
1714
1715 assert!(user_builder.unsigned_dust_fanout_tx.is_none(), "Dust isolation should NOT be active");
1717
1718 assert_eq!(user_builder.nb_outputs(), 2);
1720
1721 assert_eq!(user_builder.nb_sigs(), 3);
1723
1724 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1726 let cosign_request = user_builder.cosign_request();
1727
1728 let server_builder = CheckpointedArkoorBuilder::from_cosign_request(cosign_request)
1730 .expect("Invalid cosign request")
1731 .server_cosign(server_keypair)
1732 .expect("Incorrect key");
1733
1734 let cosign_data = server_builder.cosign_response();
1735
1736 let vtxos = user_builder
1738 .user_cosign(&alice_keypair, &cosign_data)
1739 .expect("Valid cosign data and correct key")
1740 .build_signed_vtxos();
1741
1742 assert_eq!(vtxos.len(), 2);
1744
1745 for vtxo in vtxos.into_iter() {
1746 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1748
1749 assert_eq!(vtxo.amount(), Amount::from_sat(100));
1751
1752 let mut prev_tx = funding_tx.clone();
1754 for tx in vtxo.transactions().map(|item| item.tx) {
1755 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1756 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1757 crate::test::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1758 prev_tx = tx;
1759 }
1760 }
1761 }
1762
1763 #[test]
1764 fn spend_nondust_vtxo_to_dust() {
1765 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1768 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1769 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1770
1771 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1773 amount: Amount::from_sat(500),
1774 expiry_height: 1000,
1775 exit_delta: 128,
1776 user_keypair: alice_keypair.clone(),
1777 server_keypair: server_keypair.clone()
1778 }.build();
1779
1780 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1781
1782 let outputs = vec![];
1785 let dust_outputs = vec![
1786 VtxoRequest {
1787 amount: Amount::from_sat(250),
1788 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1789 },
1790 VtxoRequest {
1791 amount: Amount::from_sat(250),
1792 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1793 }
1794 ];
1795
1796 let user_builder = CheckpointedArkoorBuilder::new_with_checkpoint(
1797 alice_vtxo.clone(),
1798 outputs,
1799 dust_outputs,
1800 ).expect("Valid arkoor request for non-dust to dust case");
1801
1802 assert!(user_builder.unsigned_dust_fanout_tx.is_none(), "Dust isolation should NOT be active");
1804
1805 assert_eq!(user_builder.nb_outputs(), 2);
1807
1808 assert_eq!(user_builder.nb_sigs(), 3);
1810
1811 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1813 let cosign_request = user_builder.cosign_request();
1814
1815 let server_builder = CheckpointedArkoorBuilder::from_cosign_request(cosign_request)
1817 .expect("Invalid cosign request")
1818 .server_cosign(server_keypair)
1819 .expect("Incorrect key");
1820
1821 let cosign_data = server_builder.cosign_response();
1822
1823 let vtxos = user_builder
1825 .user_cosign(&alice_keypair, &cosign_data)
1826 .expect("Valid cosign data and correct key")
1827 .build_signed_vtxos();
1828
1829 assert_eq!(vtxos.len(), 2);
1831
1832 for vtxo in vtxos.into_iter() {
1833 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1835
1836 assert_eq!(vtxo.amount(), Amount::from_sat(250));
1838
1839 let mut prev_tx = funding_tx.clone();
1841 for tx in vtxo.transactions().map(|item| item.tx) {
1842 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1843 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1844 crate::test::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1845 prev_tx = tx;
1846 }
1847 }
1848 }
1849}