1pub mod package;
68
69use std::marker::PhantomData;
70
71use bitcoin::hashes::Hash;
72use bitcoin::sighash::{self, SighashCache};
73use bitcoin::{
74 Amount, OutPoint, ScriptBuf, Sequence, TapSighash, TapSighashType, Transaction, TxIn, TxOut, Txid, Witness
75};
76use bitcoin::taproot::TapTweakHash;
77use bitcoin::secp256k1::{schnorr, Keypair, PublicKey};
78use bitcoin_ext::{fee, P2TR_DUST, TxOutExt};
79use secp256k1_musig::musig::PublicNonce;
80
81use crate::{musig, scripts, Vtxo, VtxoId, ServerVtxo};
82use crate::vtxo::{Full, ServerVtxoPolicy, VtxoPolicy};
83use crate::vtxo::genesis::{GenesisItem, GenesisTransition};
84
85pub use package::ArkoorPackageBuilder;
86
87
88#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
89pub enum ArkoorConstructionError {
90 #[error("Input amount of {input} does not match output amount of {output}")]
91 Unbalanced {
92 input: Amount,
93 output: Amount,
94 },
95 #[error("An output is below the dust threshold")]
96 Dust,
97 #[error("At least one output is required")]
98 NoOutputs,
99 #[error("Too many inputs provided")]
100 TooManyInputs,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
104pub enum ArkoorSigningError {
105 #[error("An error occurred while building arkoor: {0}")]
106 ArkoorConstructionError(ArkoorConstructionError),
107 #[error("Wrong number of user nonces provided. Expected {expected}, got {got}")]
108 InvalidNbUserNonces {
109 expected: usize,
110 got: usize,
111 },
112 #[error("Wrong number of server nonces provided. Expected {expected}, got {got}")]
113 InvalidNbServerNonces {
114 expected: usize,
115 got: usize,
116 },
117 #[error("Incorrect signing key provided. Expected {expected}, got {got}")]
118 IncorrectKey {
119 expected: PublicKey,
120 got: PublicKey,
121 },
122 #[error("Wrong number of server partial sigs. Expected {expected}, got {got}")]
123 InvalidNbServerPartialSigs {
124 expected: usize,
125 got: usize
126 },
127 #[error("Invalid partial signature at index {index}")]
128 InvalidPartialSignature {
129 index: usize,
130 },
131 #[error("Wrong number of packages. Expected {expected}, got {got}")]
132 InvalidNbPackages {
133 expected: usize,
134 got: usize,
135 },
136 #[error("Wrong number of keypairs. Expected {expected}, got {got}")]
137 InvalidNbKeypairs {
138 expected: usize,
139 got: usize,
140 },
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
148pub struct ArkoorDestination {
149 pub total_amount: Amount,
150 #[serde(with = "crate::encode::serde")]
151 pub policy: VtxoPolicy,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct ArkoorCosignResponse {
156 pub server_pub_nonces: Vec<musig::PublicNonce>,
157 pub server_partial_sigs: Vec<musig::PartialSignature>,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct ArkoorCosignRequest<V> {
162 pub user_pub_nonces: Vec<musig::PublicNonce>,
163 pub input: V,
164 pub outputs: Vec<ArkoorDestination>,
165 pub isolated_outputs: Vec<ArkoorDestination>,
166 pub use_checkpoint: bool,
167}
168
169impl<V> ArkoorCosignRequest<V> {
170 pub fn new(
171 user_pub_nonces: Vec<musig::PublicNonce>,
172 input: V,
173 outputs: Vec<ArkoorDestination>,
174 isolated_outputs: Vec<ArkoorDestination>,
175 use_checkpoint: bool,
176 ) -> Self {
177 Self {
178 user_pub_nonces,
179 input,
180 outputs,
181 isolated_outputs,
182 use_checkpoint,
183 }
184 }
185
186 pub fn all_outputs(&self) -> impl Iterator<Item = &ArkoorDestination> + Clone {
187 self.outputs.iter().chain(&self.isolated_outputs)
188 }
189}
190
191impl ArkoorCosignRequest<VtxoId> {
192 pub fn with_vtxo(self, vtxo: Vtxo<Full>) -> Result<ArkoorCosignRequest<Vtxo<Full>>, &'static str> {
193 if self.input != vtxo.id() {
194 return Err("Input vtxo id does not match the provided vtxo id")
195 }
196
197 Ok(ArkoorCosignRequest::new(
198 self.user_pub_nonces,
199 vtxo,
200 self.outputs,
201 self.isolated_outputs,
202 self.use_checkpoint,
203 ))
204 }
205}
206
207
208pub mod state {
209 mod sealed {
217 pub trait Sealed {}
218 impl Sealed for super::Initial {}
219 impl Sealed for super::UserGeneratedNonces {}
220 impl Sealed for super::UserSigned {}
221 impl Sealed for super::ServerCanCosign {}
222 impl Sealed for super::ServerSigned {}
223 }
224
225 pub trait BuilderState: sealed::Sealed {}
226
227 pub struct Initial;
229 impl BuilderState for Initial {}
230
231 pub struct UserGeneratedNonces;
233 impl BuilderState for UserGeneratedNonces {}
234
235 pub struct UserSigned;
237 impl BuilderState for UserSigned {}
238
239 pub struct ServerCanCosign;
241 impl BuilderState for ServerCanCosign {}
242
243
244 pub struct ServerSigned;
246 impl BuilderState for ServerSigned {}
247}
248
249pub struct ArkoorBuilder<S: state::BuilderState> {
250 input: Vtxo<Full>,
253 outputs: Vec<ArkoorDestination>,
255 isolated_outputs: Vec<ArkoorDestination>,
259
260 checkpoint_data: Option<(Transaction, Txid)>,
265 unsigned_arkoor_txs: Vec<Transaction>,
267 unsigned_isolation_fanout_tx: Option<Transaction>,
270 sighashes: Vec<TapSighash>,
272 input_tweak: TapTweakHash,
274 checkpoint_policy_tweak: TapTweakHash,
277 new_vtxo_ids: Vec<VtxoId>,
279
280 user_pub_nonces: Option<Vec<musig::PublicNonce>>,
285 user_sec_nonces: Option<Vec<musig::SecretNonce>>,
287 server_pub_nonces: Option<Vec<musig::PublicNonce>>,
289 server_partial_sigs: Option<Vec<musig::PartialSignature>>,
291 full_signatures: Option<Vec<schnorr::Signature>>,
293
294 _state: PhantomData<S>,
295}
296
297impl<S: state::BuilderState> ArkoorBuilder<S> {
298 pub fn input(&self) -> &Vtxo<Full> {
300 &self.input
301 }
302
303 pub fn normal_outputs(&self) -> &[ArkoorDestination] {
305 &self.outputs
306 }
307
308 pub fn isolated_outputs(&self) -> &[ArkoorDestination] {
310 &self.isolated_outputs
311 }
312
313 pub fn all_outputs(
315 &self,
316 ) -> impl Iterator<Item = &ArkoorDestination> + Clone {
317 self.outputs.iter().chain(&self.isolated_outputs)
318 }
319
320 fn build_checkpoint_vtxo_at(
321 &self,
322 output_idx: usize,
323 checkpoint_sig: Option<schnorr::Signature>
324 ) -> ServerVtxo<Full> {
325 let output = &self.outputs[output_idx];
326 let (checkpoint_tx, checkpoint_txid) = self.checkpoint_data.as_ref()
327 .expect("called checkpoint_vtxo_at in context without checkpoints");
328
329 Vtxo {
330 amount: output.total_amount,
331 policy: ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey()),
332 expiry_height: self.input.expiry_height,
333 server_pubkey: self.input.server_pubkey,
334 exit_delta: self.input.exit_delta,
335 point: OutPoint::new(*checkpoint_txid, output_idx as u32),
336 anchor_point: self.input.anchor_point,
337 genesis: Full {
338 items: self.input.genesis.items.clone().into_iter().chain([
339 GenesisItem {
340 transition: GenesisTransition::new_arkoor(
341 vec![self.input.user_pubkey()],
342 self.input.policy().taproot(
343 self.input.server_pubkey,
344 self.input.exit_delta,
345 self.input.expiry_height,
346 ).tap_tweak(),
347 checkpoint_sig,
348 ),
349 output_idx: output_idx as u8,
350 other_outputs: checkpoint_tx.output
351 .iter().enumerate()
352 .filter_map(|(i, txout)| {
353 if i == (output_idx as usize) || txout.is_p2a_fee_anchor() {
354 None
355 } else {
356 Some(txout.clone())
357 }
358 })
359 .collect(),
360 fee_amount: Amount::ZERO,
361 },
362 ]).collect(),
363 },
364 }
365 }
366
367 fn build_vtxo_at(
368 &self,
369 output_idx: usize,
370 checkpoint_sig: Option<schnorr::Signature>,
371 arkoor_sig: Option<schnorr::Signature>,
372 ) -> Vtxo<Full> {
373 let output = &self.outputs[output_idx];
374
375 if let Some((checkpoint_tx, _txid)) = &self.checkpoint_data {
376 let checkpoint_policy = ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey());
378
379 Vtxo {
380 amount: output.total_amount,
381 policy: output.policy.clone(),
382 expiry_height: self.input.expiry_height,
383 server_pubkey: self.input.server_pubkey,
384 exit_delta: self.input.exit_delta,
385 point: self.new_vtxo_ids[output_idx].utxo(),
386 anchor_point: self.input.anchor_point,
387 genesis: Full {
388 items: self.input.genesis.items.iter().cloned().chain([
389 GenesisItem {
390 transition: GenesisTransition::new_arkoor(
391 vec![self.input.user_pubkey()],
392 self.input.policy.taproot(
393 self.input.server_pubkey,
394 self.input.exit_delta,
395 self.input.expiry_height,
396 ).tap_tweak(),
397 checkpoint_sig,
398 ),
399 output_idx: output_idx as u8,
400 other_outputs: checkpoint_tx.output
401 .iter().enumerate()
402 .filter_map(|(i, txout)| {
403 if i == (output_idx as usize) || txout.is_p2a_fee_anchor() {
404 None
405 } else {
406 Some(txout.clone())
407 }
408 })
409 .collect(),
410 fee_amount: Amount::ZERO,
411 },
412 GenesisItem {
413 transition: GenesisTransition::new_arkoor(
414 vec![self.input.user_pubkey()],
415 checkpoint_policy.taproot(
416 self.input.server_pubkey,
417 self.input.exit_delta,
418 self.input.expiry_height,
419 ).tap_tweak(),
420 arkoor_sig,
421 ),
422 output_idx: 0,
423 other_outputs: vec![],
424 fee_amount: Amount::ZERO,
425 }
426 ]).collect(),
427 },
428 }
429 } else {
430 let arkoor_tx = &self.unsigned_arkoor_txs[0];
432
433 Vtxo {
434 amount: output.total_amount,
435 policy: output.policy.clone(),
436 expiry_height: self.input.expiry_height,
437 server_pubkey: self.input.server_pubkey,
438 exit_delta: self.input.exit_delta,
439 point: OutPoint::new(arkoor_tx.compute_txid(), output_idx as u32),
440 anchor_point: self.input.anchor_point,
441 genesis: Full {
442 items: self.input.genesis.items.iter().cloned().chain([
443 GenesisItem {
444 transition: GenesisTransition::new_arkoor(
445 vec![self.input.user_pubkey()],
446 self.input.policy.taproot(
447 self.input.server_pubkey,
448 self.input.exit_delta,
449 self.input.expiry_height,
450 ).tap_tweak(),
451 arkoor_sig,
452 ),
453 output_idx: output_idx as u8,
454 other_outputs: arkoor_tx.output
455 .iter().enumerate()
456 .filter_map(|(idx, txout)| {
457 if idx == output_idx || txout.is_p2a_fee_anchor() {
458 None
459 } else {
460 Some(txout.clone())
461 }
462 })
463 .collect(),
464 fee_amount: Amount::ZERO,
465 }
466 ]).collect(),
467 },
468 }
469 }
470 }
471
472 fn build_isolated_vtxo_at(
480 &self,
481 isolated_idx: usize,
482 pre_fanout_tx_sig: Option<schnorr::Signature>,
483 isolation_fanout_tx_sig: Option<schnorr::Signature>,
484 ) -> Vtxo<Full> {
485 let output = &self.isolated_outputs[isolated_idx];
486 let checkpoint_policy = ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey());
487
488 let fanout_tx = self.unsigned_isolation_fanout_tx.as_ref()
489 .expect("construct_dust_vtxo_at called without dust isolation");
490
491 let dust_isolation_output_idx = self.outputs.len();
493
494 if let Some((checkpoint_tx, _txid)) = &self.checkpoint_data {
495 Vtxo {
497 amount: output.total_amount,
498 policy: output.policy.clone(),
499 expiry_height: self.input.expiry_height,
500 server_pubkey: self.input.server_pubkey,
501 exit_delta: self.input.exit_delta,
502 point: OutPoint::new(fanout_tx.compute_txid(), isolated_idx as u32),
503 anchor_point: self.input.anchor_point,
504 genesis: Full {
505 items: self.input.genesis.items.iter().cloned().chain([
506 GenesisItem {
508 transition: GenesisTransition::new_arkoor(
509 vec![self.input.user_pubkey()],
510 self.input.policy.taproot(
511 self.input.server_pubkey,
512 self.input.exit_delta,
513 self.input.expiry_height,
514 ).tap_tweak(),
515 pre_fanout_tx_sig,
516 ),
517 output_idx: dust_isolation_output_idx as u8,
518 other_outputs: checkpoint_tx.output
521 .iter().enumerate()
522 .filter_map(|(idx, txout)| {
523 let is_p2a = txout.is_p2a_fee_anchor();
524 if idx == dust_isolation_output_idx || is_p2a {
525 None
526 } else {
527 Some(txout.clone())
528 }
529 })
530 .collect(),
531 fee_amount: Amount::ZERO,
532 },
533 GenesisItem {
535 transition: GenesisTransition::new_arkoor(
536 vec![self.input.user_pubkey()],
537 checkpoint_policy.taproot(
538 self.input.server_pubkey,
539 self.input.exit_delta,
540 self.input.expiry_height,
541 ).tap_tweak(),
542 isolation_fanout_tx_sig,
543 ),
544 output_idx: isolated_idx as u8,
545 other_outputs: fanout_tx.output
548 .iter().enumerate()
549 .filter_map(|(idx, txout)| {
550 if idx == isolated_idx || txout.is_p2a_fee_anchor() {
551 None
552 } else {
553 Some(txout.clone())
554 }
555 })
556 .collect(),
557 fee_amount: Amount::ZERO,
558 },
559 ]).collect(),
560 },
561 }
562 } else {
563 let arkoor_tx = &self.unsigned_arkoor_txs[0];
565
566 Vtxo {
567 amount: output.total_amount,
568 policy: output.policy.clone(),
569 expiry_height: self.input.expiry_height,
570 server_pubkey: self.input.server_pubkey,
571 exit_delta: self.input.exit_delta,
572 point: OutPoint::new(fanout_tx.compute_txid(), isolated_idx as u32),
573 anchor_point: self.input.anchor_point,
574 genesis: Full {
575 items: self.input.genesis.items.iter().cloned().chain([
576 GenesisItem {
578 transition: GenesisTransition::new_arkoor(
579 vec![self.input.user_pubkey()],
580 self.input.policy.taproot(
581 self.input.server_pubkey,
582 self.input.exit_delta,
583 self.input.expiry_height,
584 ).tap_tweak(),
585 pre_fanout_tx_sig,
586 ),
587 output_idx: dust_isolation_output_idx as u8,
588 other_outputs: arkoor_tx.output
589 .iter().enumerate()
590 .filter_map(|(idx, txout)| {
591 if idx == dust_isolation_output_idx || txout.is_p2a_fee_anchor() {
592 None
593 } else {
594 Some(txout.clone())
595 }
596 })
597 .collect(),
598 fee_amount: Amount::ZERO,
599 },
600 GenesisItem {
602 transition: GenesisTransition::new_arkoor(
603 vec![self.input.user_pubkey()],
604 checkpoint_policy.taproot(
605 self.input.server_pubkey,
606 self.input.exit_delta,
607 self.input.expiry_height,
608 ).tap_tweak(),
609 isolation_fanout_tx_sig,
610 ),
611 output_idx: isolated_idx as u8,
612 other_outputs: fanout_tx.output
613 .iter().enumerate()
614 .filter_map(|(idx, txout)| {
615 if idx == isolated_idx || txout.is_p2a_fee_anchor() {
616 None
617 } else {
618 Some(txout.clone())
619 }
620 })
621 .collect(),
622 fee_amount: Amount::ZERO,
623 },
624 ]).collect(),
625 },
626 }
627 }
628 }
629
630 fn nb_sigs(&self) -> usize {
631 let base = if self.checkpoint_data.is_some() {
632 1 + self.outputs.len() } else {
634 1 };
636
637 if self.unsigned_isolation_fanout_tx.is_some() {
638 base + 1 } else {
640 base
641 }
642 }
643
644 pub fn build_unsigned_vtxos<'a>(&'a self) -> impl Iterator<Item = Vtxo<Full>> + 'a {
645 let regular = (0..self.outputs.len()).map(|i| self.build_vtxo_at(i, None, None));
646 let isolated = (0..self.isolated_outputs.len())
647 .map(|i| self.build_isolated_vtxo_at(i, None, None));
648 regular.chain(isolated)
649 }
650
651 pub fn build_unsigned_internal_vtxos<'a>(&'a self) -> impl Iterator<Item = ServerVtxo<Full>> + 'a {
656 let checkpoint_vtxos = {
657 let range = if self.checkpoint_data.is_some() {
658 0..self.outputs.len()
659 } else {
660 0..0
662 };
663 range.map(|i| self.build_checkpoint_vtxo_at(i, None))
664 };
665
666 let isolation_vtxo = if !self.isolated_outputs.is_empty() {
667 let output_idx = self.outputs.len();
669
670 let (int_tx, int_txid) = if let Some((tx, txid)) = &self.checkpoint_data {
672 (tx, *txid)
673 } else {
674 let arkoor_tx = &self.unsigned_arkoor_txs[0];
675 (arkoor_tx, arkoor_tx.compute_txid())
676 };
677
678 Some(Vtxo {
679 amount: self.isolated_outputs.iter().map(|o| o.total_amount).sum(),
680 policy: ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey()),
681 expiry_height: self.input.expiry_height,
682 server_pubkey: self.input.server_pubkey,
683 exit_delta: self.input.exit_delta,
684 point: OutPoint::new(int_txid, output_idx as u32),
685 anchor_point: self.input.anchor_point,
686 genesis: Full {
687 items: self.input.genesis.items.clone().into_iter().chain([
688 GenesisItem {
689 transition: GenesisTransition::new_arkoor(
690 vec![self.input.user_pubkey()],
691 self.input_tweak,
692 None,
693 ),
694 output_idx: output_idx as u8,
695 other_outputs: int_tx.output.iter().enumerate()
696 .filter_map(|(i, txout)| {
697 if i == output_idx || txout.is_p2a_fee_anchor() {
698 None
699 } else {
700 Some(txout.clone())
701 }
702 })
703 .collect(),
704 fee_amount: Amount::ZERO,
705 },
706 ]).collect(),
707 },
708 })
709 } else {
710 None
711 };
712
713 checkpoint_vtxos.chain(isolation_vtxo)
714 }
715
716 pub fn spend_info(&self) -> Vec<(VtxoId, Txid)> {
718 let mut ret = Vec::with_capacity(1 + self.outputs.len());
719
720 if let Some((_tx, checkpoint_txid)) = &self.checkpoint_data {
721 ret.push((self.input.id(), *checkpoint_txid));
723
724 for idx in 0..self.outputs.len() {
726 ret.push((
727 VtxoId::from(OutPoint::new(*checkpoint_txid, idx as u32)),
728 self.unsigned_arkoor_txs[idx].compute_txid()
729 ));
730 }
731
732 if let Some(fanout_tx) = &self.unsigned_isolation_fanout_tx {
734 let fanout_txid = fanout_tx.compute_txid();
735
736 let isolated_output_idx = self.outputs.len() as u32;
738 ret.push((
739 VtxoId::from(OutPoint::new(*checkpoint_txid, isolated_output_idx)),
740 fanout_txid,
741 ));
742 }
743 } else {
744 let arkoor_txid = self.unsigned_arkoor_txs[0].compute_txid();
745
746 ret.push((self.input.id(), arkoor_txid));
748
749 if let Some(fanout_tx) = &self.unsigned_isolation_fanout_tx {
751 let fanout_txid = fanout_tx.compute_txid();
752
753 let isolation_output_idx = self.outputs.len() as u32;
755 ret.push((
756 VtxoId::from(OutPoint::new(arkoor_txid, isolation_output_idx)),
757 fanout_txid,
758 ));
759 }
760 }
761
762 ret
763 }
764
765 pub fn virtual_transactions(&self) -> Vec<Txid> {
770 let mut ret = Vec::new();
771 if let Some((_, txid)) = &self.checkpoint_data {
773 ret.push(*txid);
774 }
775 ret.extend(self.unsigned_arkoor_txs.iter().map(|tx| tx.compute_txid()));
777 if let Some(tx) = &self.unsigned_isolation_fanout_tx {
779 ret.push(tx.compute_txid());
780 }
781 ret
782 }
783
784 fn taptweak_at(&self, idx: usize) -> TapTweakHash {
785 if idx == 0 { self.input_tweak } else { self.checkpoint_policy_tweak }
786 }
787
788 fn user_pubkey(&self) -> PublicKey {
789 self.input.user_pubkey()
790 }
791
792 fn server_pubkey(&self) -> PublicKey {
793 self.input.server_pubkey()
794 }
795
796 fn construct_unsigned_checkpoint_tx<G>(
801 input: &Vtxo<G>,
802 outputs: &[ArkoorDestination],
803 dust_isolation_amount: Option<Amount>,
804 ) -> Transaction {
805
806 let output_policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
808 let checkpoint_spk = output_policy
809 .script_pubkey(input.server_pubkey(), input.exit_delta(), input.expiry_height());
810
811 Transaction {
812 version: bitcoin::transaction::Version(3),
813 lock_time: bitcoin::absolute::LockTime::ZERO,
814 input: vec![TxIn {
815 previous_output: input.point(),
816 script_sig: ScriptBuf::new(),
817 sequence: Sequence::ZERO,
818 witness: Witness::new(),
819 }],
820 output: outputs.iter().map(|o| {
821 TxOut {
822 value: o.total_amount,
823 script_pubkey: checkpoint_spk.clone(),
824 }
825 })
826 .chain(dust_isolation_amount.map(|amt| {
828 TxOut {
829 value: amt,
830 script_pubkey: checkpoint_spk.clone(),
831 }
832 }))
833 .chain([fee::fee_anchor()]).collect()
834 }
835 }
836
837 fn construct_unsigned_arkoor_txs<G>(
838 input: &Vtxo<G>,
839 outputs: &[ArkoorDestination],
840 checkpoint_txid: Option<Txid>,
841 dust_isolation_amount: Option<Amount>,
842 ) -> Vec<Transaction> {
843
844 if let Some(checkpoint_txid) = checkpoint_txid {
845 let mut arkoor_txs = Vec::with_capacity(outputs.len());
847
848 for (vout, output) in outputs.iter().enumerate() {
849 let transaction = Transaction {
850 version: bitcoin::transaction::Version(3),
851 lock_time: bitcoin::absolute::LockTime::ZERO,
852 input: vec![TxIn {
853 previous_output: OutPoint::new(checkpoint_txid, vout as u32),
854 script_sig: ScriptBuf::new(),
855 sequence: Sequence::ZERO,
856 witness: Witness::new(),
857 }],
858 output: vec![
859 output.policy.txout(
860 output.total_amount,
861 input.server_pubkey(),
862 input.exit_delta(),
863 input.expiry_height(),
864 ),
865 fee::fee_anchor(),
866 ]
867 };
868 arkoor_txs.push(transaction);
869 }
870
871 arkoor_txs
872 } else {
873 let checkpoint_policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
875 let checkpoint_spk = checkpoint_policy.script_pubkey(
876 input.server_pubkey(),
877 input.exit_delta(),
878 input.expiry_height()
879 );
880
881 let transaction = Transaction {
882 version: bitcoin::transaction::Version(3),
883 lock_time: bitcoin::absolute::LockTime::ZERO,
884 input: vec![TxIn {
885 previous_output: input.point(),
886 script_sig: ScriptBuf::new(),
887 sequence: Sequence::ZERO,
888 witness: Witness::new(),
889 }],
890 output: outputs.iter()
891 .map(|o| o.policy.txout(
892 o.total_amount,
893 input.server_pubkey(),
894 input.exit_delta(),
895 input.expiry_height(),
896 ))
897 .chain(dust_isolation_amount.map(|amt| TxOut {
899 value: amt,
900 script_pubkey: checkpoint_spk.clone(),
901 }))
902 .chain([fee::fee_anchor()])
903 .collect()
904 };
905 vec![transaction]
906 }
907 }
908
909 fn construct_unsigned_isolation_fanout_tx<G>(
917 input: &Vtxo<G>,
918 isolated_outputs: &[ArkoorDestination],
919 parent_txid: Txid, dust_isolation_output_vout: u32, ) -> Transaction {
922
923 Transaction {
924 version: bitcoin::transaction::Version(3),
925 lock_time: bitcoin::absolute::LockTime::ZERO,
926 input: vec![TxIn {
927 previous_output: OutPoint::new(parent_txid, dust_isolation_output_vout),
928 script_sig: ScriptBuf::new(),
929 sequence: Sequence::ZERO,
930 witness: Witness::new(),
931 }],
932 output: isolated_outputs.iter().map(|o| {
933 TxOut {
934 value: o.total_amount,
935 script_pubkey: o.policy.script_pubkey(
936 input.server_pubkey(),
937 input.exit_delta(),
938 input.expiry_height(),
939 ),
940 }
941 }).chain([fee::fee_anchor()]).collect(),
942 }
943 }
944
945 fn validate_amounts<G>(
946 input: &Vtxo<G>,
947 outputs: &[ArkoorDestination],
948 isolation_outputs: &[ArkoorDestination],
949 ) -> Result<(), ArkoorConstructionError> {
950
951 let input_amount = input.amount();
956 let output_amount = outputs.iter().chain(isolation_outputs.iter())
957 .map(|o| o.total_amount).sum::<Amount>();
958
959 if input_amount != output_amount {
960 return Err(ArkoorConstructionError::Unbalanced {
961 input: input_amount,
962 output: output_amount,
963 })
964 }
965
966 if outputs.is_empty() {
968 return Err(ArkoorConstructionError::NoOutputs)
969 }
970
971 if !isolation_outputs.is_empty() {
973 let isolation_sum: Amount = isolation_outputs.iter()
974 .map(|o| o.total_amount).sum();
975 if isolation_sum < P2TR_DUST {
976 return Err(ArkoorConstructionError::Dust)
977 }
978 }
979
980 Ok(())
981 }
982
983
984 fn to_state<S2: state::BuilderState>(self) -> ArkoorBuilder<S2> {
985 ArkoorBuilder {
986 input: self.input,
987 outputs: self.outputs,
988 isolated_outputs: self.isolated_outputs,
989 checkpoint_data: self.checkpoint_data,
990 unsigned_arkoor_txs: self.unsigned_arkoor_txs,
991 unsigned_isolation_fanout_tx: self.unsigned_isolation_fanout_tx,
992 new_vtxo_ids: self.new_vtxo_ids,
993 sighashes: self.sighashes,
994 input_tweak: self.input_tweak,
995 checkpoint_policy_tweak: self.checkpoint_policy_tweak,
996 user_pub_nonces: self.user_pub_nonces,
997 user_sec_nonces: self.user_sec_nonces,
998 server_pub_nonces: self.server_pub_nonces,
999 server_partial_sigs: self.server_partial_sigs,
1000 full_signatures: self.full_signatures,
1001 _state: PhantomData,
1002 }
1003 }
1004}
1005
1006impl ArkoorBuilder<state::Initial> {
1007 pub fn new_with_checkpoint(
1009 input: Vtxo<Full>,
1010 outputs: Vec<ArkoorDestination>,
1011 isolated_outputs: Vec<ArkoorDestination>,
1012 ) -> Result<Self, ArkoorConstructionError> {
1013 Self::new(input, outputs, isolated_outputs, true)
1014 }
1015
1016 pub fn new_without_checkpoint(
1018 input: Vtxo<Full>,
1019 outputs: Vec<ArkoorDestination>,
1020 isolated_outputs: Vec<ArkoorDestination>,
1021 ) -> Result<Self, ArkoorConstructionError> {
1022 Self::new(input, outputs, isolated_outputs, false)
1023 }
1024
1025 pub fn new_with_checkpoint_isolate_dust(
1030 input: Vtxo<Full>,
1031 outputs: Vec<ArkoorDestination>,
1032 ) -> Result<Self, ArkoorConstructionError> {
1033 Self::new_isolate_dust(input, outputs, true)
1034 }
1035
1036 pub(crate) fn new_isolate_dust(
1037 input: Vtxo<Full>,
1038 outputs: Vec<ArkoorDestination>,
1039 use_checkpoints: bool,
1040 ) -> Result<Self, ArkoorConstructionError> {
1041 if outputs.iter().all(|v| v.total_amount >= P2TR_DUST)
1043 || outputs.iter().all(|v| v.total_amount < P2TR_DUST)
1044 {
1045 return Self::new(input, outputs, vec![], use_checkpoints);
1046 }
1047
1048 let (mut dust, mut non_dust) = outputs.iter().cloned()
1050 .partition::<Vec<_>, _>(|v| v.total_amount < P2TR_DUST);
1051
1052 let dust_sum = dust.iter().map(|o| o.total_amount).sum::<Amount>();
1053 if dust_sum >= P2TR_DUST {
1054 return Self::new(input, non_dust, dust, use_checkpoints);
1055 }
1056
1057 let non_dust_sum = non_dust.iter().map(|o| o.total_amount).sum::<Amount>();
1059 if non_dust_sum < P2TR_DUST * 2 {
1060 return Self::new(input, outputs, vec![], use_checkpoints);
1061 }
1062
1063 let deficit = P2TR_DUST - dust_sum;
1065 let split_idx = non_dust.iter()
1068 .position(|o| o.total_amount - deficit >= P2TR_DUST);
1069
1070 if let Some(idx) = split_idx {
1071 let output_to_split = non_dust[idx].clone();
1072
1073 let dust_piece = ArkoorDestination {
1074 total_amount: deficit,
1075 policy: output_to_split.policy.clone(),
1076 };
1077 let leftover = ArkoorDestination {
1078 total_amount: output_to_split.total_amount - deficit,
1079 policy: output_to_split.policy,
1080 };
1081
1082 non_dust[idx] = leftover;
1083 dust.insert(0, dust_piece);
1085
1086 return Self::new(input, non_dust, dust, use_checkpoints);
1087 } else {
1088 let all_outputs = non_dust.into_iter().chain(dust).collect();
1090 return Self::new(input, all_outputs, vec![], use_checkpoints);
1091 }
1092 }
1093
1094 pub(crate) fn new(
1095 input: Vtxo<Full>,
1096 outputs: Vec<ArkoorDestination>,
1097 isolated_outputs: Vec<ArkoorDestination>,
1098 use_checkpoint: bool,
1099 ) -> Result<Self, ArkoorConstructionError> {
1100 Self::validate_amounts(&input, &outputs, &isolated_outputs)?;
1102
1103 let combined_dust_amount = if !isolated_outputs.is_empty() {
1105 Some(isolated_outputs.iter().map(|o| o.total_amount).sum())
1106 } else {
1107 None
1108 };
1109
1110 let unsigned_checkpoint_tx = if use_checkpoint {
1112 let tx = Self::construct_unsigned_checkpoint_tx(
1113 &input,
1114 &outputs,
1115 combined_dust_amount,
1116 );
1117 let txid = tx.compute_txid();
1118 Some((tx, txid))
1119 } else {
1120 None
1121 };
1122
1123 let unsigned_arkoor_txs = Self::construct_unsigned_arkoor_txs(
1125 &input,
1126 &outputs,
1127 unsigned_checkpoint_tx.as_ref().map(|t| t.1),
1128 combined_dust_amount,
1129 );
1130
1131 let unsigned_isolation_fanout_tx = if !isolated_outputs.is_empty() {
1133 let dust_isolation_output_vout = outputs.len() as u32;
1136
1137 let parent_txid = if let Some((_tx, txid)) = &unsigned_checkpoint_tx {
1138 *txid
1139 } else {
1140 unsigned_arkoor_txs[0].compute_txid()
1141 };
1142
1143 Some(Self::construct_unsigned_isolation_fanout_tx(
1144 &input,
1145 &isolated_outputs,
1146 parent_txid,
1147 dust_isolation_output_vout,
1148 ))
1149 } else {
1150 None
1151 };
1152
1153 let new_vtxo_ids = unsigned_arkoor_txs.iter()
1155 .map(|tx| OutPoint::new(tx.compute_txid(), 0))
1156 .map(|outpoint| VtxoId::from(outpoint))
1157 .collect();
1158
1159 let mut sighashes = Vec::new();
1161
1162 if let Some((checkpoint_tx, _txid)) = &unsigned_checkpoint_tx {
1163 sighashes.push(arkoor_sighash(&input.txout(), checkpoint_tx));
1165
1166 for vout in 0..outputs.len() {
1168 let prevout = checkpoint_tx.output[vout].clone();
1169 sighashes.push(arkoor_sighash(&prevout, &unsigned_arkoor_txs[vout]));
1170 }
1171 } else {
1172 sighashes.push(arkoor_sighash(&input.txout(), &unsigned_arkoor_txs[0]));
1174 }
1175
1176 if let Some(ref tx) = unsigned_isolation_fanout_tx {
1178 let dust_output_vout = outputs.len(); let prevout = if let Some((checkpoint_tx, _txid)) = &unsigned_checkpoint_tx {
1180 checkpoint_tx.output[dust_output_vout].clone()
1181 } else {
1182 unsigned_arkoor_txs[0].output[dust_output_vout].clone()
1184 };
1185 sighashes.push(arkoor_sighash(&prevout, tx));
1186 }
1187
1188 let policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
1190 let input_tweak = input.output_taproot().tap_tweak();
1191 let checkpoint_policy_tweak = policy.taproot(
1192 input.server_pubkey(),
1193 input.exit_delta(),
1194 input.expiry_height(),
1195 ).tap_tweak();
1196
1197 Ok(Self {
1198 input: input,
1199 outputs: outputs,
1200 isolated_outputs,
1201 sighashes: sighashes,
1202 input_tweak,
1203 checkpoint_policy_tweak,
1204 checkpoint_data: unsigned_checkpoint_tx,
1205 unsigned_arkoor_txs: unsigned_arkoor_txs,
1206 unsigned_isolation_fanout_tx,
1207 new_vtxo_ids: new_vtxo_ids,
1208 user_pub_nonces: None,
1209 user_sec_nonces: None,
1210 server_pub_nonces: None,
1211 server_partial_sigs: None,
1212 full_signatures: None,
1213 _state: PhantomData,
1214 })
1215 }
1216
1217 pub fn generate_user_nonces(
1220 mut self,
1221 user_keypair: Keypair,
1222 ) -> ArkoorBuilder<state::UserGeneratedNonces> {
1223 let mut user_pub_nonces = Vec::with_capacity(self.nb_sigs());
1224 let mut user_sec_nonces = Vec::with_capacity(self.nb_sigs());
1225
1226 for idx in 0..self.nb_sigs() {
1227 let sighash = &self.sighashes[idx].to_byte_array();
1228 let (sec_nonce, pub_nonce) = musig::nonce_pair_with_msg(&user_keypair, sighash);
1229
1230 user_pub_nonces.push(pub_nonce);
1231 user_sec_nonces.push(sec_nonce);
1232 }
1233
1234 self.user_pub_nonces = Some(user_pub_nonces);
1235 self.user_sec_nonces = Some(user_sec_nonces);
1236
1237 self.to_state::<state::UserGeneratedNonces>()
1238 }
1239
1240 fn set_user_pub_nonces(
1247 mut self,
1248 user_pub_nonces: Vec<musig::PublicNonce>,
1249 ) -> Result<ArkoorBuilder<state::ServerCanCosign>, ArkoorSigningError> {
1250 if user_pub_nonces.len() != self.nb_sigs() {
1251 return Err(ArkoorSigningError::InvalidNbUserNonces {
1252 expected: self.nb_sigs(),
1253 got: user_pub_nonces.len()
1254 })
1255 }
1256
1257 self.user_pub_nonces = Some(user_pub_nonces);
1258 Ok(self.to_state::<state::ServerCanCosign>())
1259 }
1260}
1261
1262impl<'a> ArkoorBuilder<state::ServerCanCosign> {
1263 pub fn from_cosign_request(
1264 cosign_request: ArkoorCosignRequest<Vtxo<Full>>,
1265 ) -> Result<ArkoorBuilder<state::ServerCanCosign>, ArkoorSigningError> {
1266 let ret = ArkoorBuilder::new(
1267 cosign_request.input,
1268 cosign_request.outputs,
1269 cosign_request.isolated_outputs,
1270 cosign_request.use_checkpoint,
1271 )
1272 .map_err(ArkoorSigningError::ArkoorConstructionError)?
1273 .set_user_pub_nonces(cosign_request.user_pub_nonces.clone())?;
1274 Ok(ret)
1275 }
1276
1277 pub fn server_cosign(
1278 mut self,
1279 server_keypair: &Keypair,
1280 ) -> Result<ArkoorBuilder<state::ServerSigned>, ArkoorSigningError> {
1281 if server_keypair.public_key() != self.input.server_pubkey() {
1283 return Err(ArkoorSigningError::IncorrectKey {
1284 expected: self.input.server_pubkey(),
1285 got: server_keypair.public_key(),
1286 });
1287 }
1288
1289 let mut server_pub_nonces = Vec::with_capacity(self.outputs.len() + 1);
1290 let mut server_partial_sigs = Vec::with_capacity(self.outputs.len() + 1);
1291
1292 for idx in 0..self.nb_sigs() {
1293 let (server_pub_nonce, server_partial_sig) = musig::deterministic_partial_sign(
1294 &server_keypair,
1295 [self.input.user_pubkey()],
1296 &[&self.user_pub_nonces.as_ref().expect("state-invariant")[idx]],
1297 self.sighashes[idx].to_byte_array(),
1298 Some(self.taptweak_at(idx).to_byte_array()),
1299 );
1300
1301 server_pub_nonces.push(server_pub_nonce);
1302 server_partial_sigs.push(server_partial_sig);
1303 };
1304
1305 self.server_pub_nonces = Some(server_pub_nonces);
1306 self.server_partial_sigs = Some(server_partial_sigs);
1307 Ok(self.to_state::<state::ServerSigned>())
1308 }
1309}
1310
1311impl ArkoorBuilder<state::ServerSigned> {
1312 pub fn user_pub_nonces(&self) -> Vec<musig::PublicNonce> {
1313 self.user_pub_nonces.as_ref().expect("state invariant").clone()
1314 }
1315
1316 pub fn server_partial_signatures(&self) -> Vec<musig::PartialSignature> {
1317 self.server_partial_sigs.as_ref().expect("state invariant").clone()
1318 }
1319
1320 pub fn cosign_response(&self) -> ArkoorCosignResponse {
1321 ArkoorCosignResponse {
1322 server_pub_nonces: self.server_pub_nonces.as_ref()
1323 .expect("state invariant").clone(),
1324 server_partial_sigs: self.server_partial_sigs.as_ref()
1325 .expect("state invariant").clone(),
1326 }
1327 }
1328}
1329
1330impl ArkoorBuilder<state::UserGeneratedNonces> {
1331 pub fn user_pub_nonces(&self) -> &[PublicNonce] {
1332 self.user_pub_nonces.as_ref().expect("State invariant")
1333 }
1334
1335 pub fn cosign_request(&self) -> ArkoorCosignRequest<Vtxo<Full>> {
1336 ArkoorCosignRequest {
1337 user_pub_nonces: self.user_pub_nonces().to_vec(),
1338 input: self.input.clone(),
1339 outputs: self.outputs.clone(),
1340 isolated_outputs: self.isolated_outputs.clone(),
1341 use_checkpoint: self.checkpoint_data.is_some(),
1342 }
1343 }
1344
1345 fn validate_server_cosign_response(
1346 &self,
1347 data: &ArkoorCosignResponse,
1348 ) -> Result<(), ArkoorSigningError> {
1349
1350 if data.server_pub_nonces.len() != self.nb_sigs() {
1352 return Err(ArkoorSigningError::InvalidNbServerNonces {
1353 expected: self.nb_sigs(),
1354 got: data.server_pub_nonces.len(),
1355 });
1356 }
1357
1358 if data.server_partial_sigs.len() != self.nb_sigs() {
1359 return Err(ArkoorSigningError::InvalidNbServerPartialSigs {
1360 expected: self.nb_sigs(),
1361 got: data.server_partial_sigs.len(),
1362 })
1363 }
1364
1365 for idx in 0..self.nb_sigs() {
1367 let is_valid_sig = scripts::verify_partial_sig(
1368 self.sighashes[idx],
1369 self.taptweak_at(idx),
1370 (self.input.server_pubkey(), &data.server_pub_nonces[idx]),
1371 (self.input.user_pubkey(), &self.user_pub_nonces()[idx]),
1372 &data.server_partial_sigs[idx]
1373 );
1374
1375 if !is_valid_sig {
1376 return Err(ArkoorSigningError::InvalidPartialSignature {
1377 index: idx,
1378 });
1379 }
1380 }
1381 Ok(())
1382 }
1383
1384 pub fn user_cosign(
1385 mut self,
1386 user_keypair: &Keypair,
1387 server_cosign_data: &ArkoorCosignResponse,
1388 ) -> Result<ArkoorBuilder<state::UserSigned>, ArkoorSigningError> {
1389 if user_keypair.public_key() != self.input.user_pubkey() {
1391 return Err(ArkoorSigningError::IncorrectKey {
1392 expected: self.input.user_pubkey(),
1393 got: user_keypair.public_key(),
1394 });
1395 }
1396
1397 self.validate_server_cosign_response(&server_cosign_data)?;
1399
1400 let mut sigs = Vec::with_capacity(self.nb_sigs());
1401
1402 let user_sec_nonces = self.user_sec_nonces.take().expect("state invariant");
1405
1406 for (idx, user_sec_nonce) in user_sec_nonces.into_iter().enumerate() {
1407 let user_pub_nonce = self.user_pub_nonces()[idx];
1408 let server_pub_nonce = server_cosign_data.server_pub_nonces[idx];
1409 let agg_nonce = musig::nonce_agg(&[&user_pub_nonce, &server_pub_nonce]);
1410
1411 let (_partial, maybe_sig) = musig::partial_sign(
1412 [self.user_pubkey(), self.server_pubkey()],
1413 agg_nonce,
1414 &user_keypair,
1415 user_sec_nonce,
1416 self.sighashes[idx].to_byte_array(),
1417 Some(self.taptweak_at(idx).to_byte_array()),
1418 Some(&[&server_cosign_data.server_partial_sigs[idx]])
1419 );
1420
1421 let sig = maybe_sig.expect("The full signature exists. The server did sign first");
1422 sigs.push(sig);
1423 }
1424
1425 self.full_signatures = Some(sigs);
1426
1427 Ok(self.to_state::<state::UserSigned>())
1428 }
1429}
1430
1431
1432impl<'a> ArkoorBuilder<state::UserSigned> {
1433 pub fn build_signed_vtxos(&self) -> Vec<Vtxo<Full>> {
1434 let sigs = self.full_signatures.as_ref().expect("state invariant");
1435 let mut ret = Vec::with_capacity(self.outputs.len() + self.isolated_outputs.len());
1436
1437 if self.checkpoint_data.is_some() {
1438 let checkpoint_sig = sigs[0];
1439
1440 for i in 0..self.outputs.len() {
1442 let arkoor_sig = sigs[1 + i];
1443 ret.push(self.build_vtxo_at(i, Some(checkpoint_sig), Some(arkoor_sig)));
1444 }
1445
1446 if self.unsigned_isolation_fanout_tx.is_some() {
1448 let m = self.outputs.len();
1449 let fanout_tx_sig = sigs[1 + m];
1450
1451 for i in 0..self.isolated_outputs.len() {
1452 ret.push(self.build_isolated_vtxo_at(
1453 i,
1454 Some(checkpoint_sig),
1455 Some(fanout_tx_sig),
1456 ));
1457 }
1458 }
1459 } else {
1460 let arkoor_sig = sigs[0];
1462
1463 for i in 0..self.outputs.len() {
1465 ret.push(self.build_vtxo_at(i, None, Some(arkoor_sig)));
1466 }
1467
1468 if self.unsigned_isolation_fanout_tx.is_some() {
1470 let fanout_tx_sig = sigs[1];
1471
1472 for i in 0..self.isolated_outputs.len() {
1473 ret.push(self.build_isolated_vtxo_at(
1474 i,
1475 Some(arkoor_sig), Some(fanout_tx_sig),
1477 ));
1478 }
1479 }
1480 }
1481
1482 ret
1483 }
1484}
1485
1486fn arkoor_sighash(prevout: &TxOut, arkoor_tx: &Transaction) -> TapSighash {
1487 let mut shc = SighashCache::new(arkoor_tx);
1488
1489 shc.taproot_key_spend_signature_hash(
1490 0, &sighash::Prevouts::All(&[prevout]), TapSighashType::Default,
1491 ).expect("sighash error")
1492}
1493
1494#[cfg(test)]
1495mod test {
1496 use super::*;
1497
1498 use std::collections::HashSet;
1499
1500 use bitcoin::Amount;
1501 use bitcoin::secp256k1::Keypair;
1502 use bitcoin::secp256k1::rand;
1503
1504 use crate::SECP;
1505 use crate::test_util::dummy::DummyTestVtxoSpec;
1506 use crate::vtxo::VtxoId;
1507
1508 fn verify_builder<S: state::BuilderState>(
1510 builder: &ArkoorBuilder<S>,
1511 input: &Vtxo<Full>,
1512 outputs: &[ArkoorDestination],
1513 isolated_outputs: &[ArkoorDestination],
1514 ) {
1515 let has_isolation = !isolated_outputs.is_empty();
1516
1517 let spend_info = builder.spend_info();
1518 let spend_vtxo_ids: HashSet<VtxoId> = spend_info.iter().map(|(id, _)| *id).collect();
1519
1520 assert_eq!(spend_info[0].0, input.id());
1522
1523 assert_eq!(spend_vtxo_ids.len(), spend_info.len());
1525
1526 let internal_vtxos = builder.build_unsigned_internal_vtxos().collect::<Vec<_>>();
1528 let internal_vtxo_ids = internal_vtxos.iter().map(|v| v.id()).collect::<HashSet<_>>();
1529 for internal_vtxo in &internal_vtxos {
1530 assert!(spend_vtxo_ids.contains(&internal_vtxo.id()));
1531 assert!(matches!(internal_vtxo.policy(), ServerVtxoPolicy::Checkpoint(_)));
1532 }
1533
1534 for (vtxo_id, _) in &spend_info[1..] {
1536 assert!(internal_vtxo_ids.contains(vtxo_id));
1537 }
1538
1539 if has_isolation {
1541 let isolation_vtxo = internal_vtxos.last().unwrap();
1542 let expected_isolation_amount: Amount = isolated_outputs.iter()
1543 .map(|o| o.total_amount)
1544 .sum();
1545 assert_eq!(isolation_vtxo.amount(), expected_isolation_amount);
1546 }
1547
1548 let final_vtxos = builder.build_unsigned_vtxos().collect::<Vec<_>>();
1550 for final_vtxo in &final_vtxos {
1551 assert!(!spend_vtxo_ids.contains(&final_vtxo.id()));
1552 }
1553
1554 let all_destinations = outputs.iter()
1556 .chain(isolated_outputs.iter())
1557 .collect::<Vec<&_>>();
1558 for (vtxo, dest) in final_vtxos.iter().zip(all_destinations.iter()) {
1559 assert_eq!(vtxo.amount(), dest.total_amount);
1560 assert_eq!(vtxo.policy, dest.policy);
1561 }
1562
1563 let total_output_amount: Amount = final_vtxos.iter().map(|v| v.amount()).sum();
1565 assert_eq!(total_output_amount, input.amount());
1566 }
1567
1568 #[test]
1569 fn build_checkpointed_arkoor() {
1570 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1571 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1572 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1573
1574 println!("Alice keypair: {}", alice_keypair.public_key());
1575 println!("Bob keypair: {}", bob_keypair.public_key());
1576 println!("Server keypair: {}", server_keypair.public_key());
1577 println!("-----------------------------------------------");
1578
1579 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1580 amount: Amount::from_sat(100_330),
1581 fee: Amount::from_sat(330),
1582 expiry_height: 1000,
1583 exit_delta : 128,
1584 user_keypair: alice_keypair.clone(),
1585 server_keypair: server_keypair.clone()
1586 }.build();
1587
1588 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1590
1591 let dest = vec![
1592 ArkoorDestination {
1593 total_amount: Amount::from_sat(96_000),
1594 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1595 },
1596 ArkoorDestination {
1597 total_amount: Amount::from_sat(4_000),
1598 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1599 }
1600 ];
1601
1602 let user_builder = ArkoorBuilder::new_with_checkpoint(
1603 alice_vtxo.clone(),
1604 dest.clone(),
1605 vec![], ).expect("Valid arkoor request");
1607
1608 verify_builder(&user_builder, &alice_vtxo, &dest, &[]);
1609
1610 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1611 let cosign_request = user_builder.cosign_request();
1612
1613 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1615 .expect("Invalid cosign request")
1616 .server_cosign(&server_keypair)
1617 .expect("Incorrect key");
1618
1619 let cosign_data = server_builder.cosign_response();
1620
1621 let vtxos = user_builder
1623 .user_cosign(&alice_keypair, &cosign_data)
1624 .expect("Valid cosign data and correct key")
1625 .build_signed_vtxos();
1626
1627 for vtxo in vtxos.into_iter() {
1628 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1630
1631 let mut prev_tx = funding_tx.clone();
1633 for tx in vtxo.transactions().map(|item| item.tx) {
1634 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1635 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1636 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1637 prev_tx = tx;
1638 }
1639 }
1640
1641 }
1642
1643 #[test]
1644 fn build_checkpointed_arkoor_with_dust_isolation() {
1645 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1648 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1649 let charlie_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1650 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1651
1652 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1653 amount: Amount::from_sat(100_330),
1654 fee: Amount::from_sat(330),
1655 expiry_height: 1000,
1656 exit_delta : 128,
1657 user_keypair: alice_keypair.clone(),
1658 server_keypair: server_keypair.clone()
1659 }.build();
1660
1661 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1663
1664 let outputs = vec![
1666 ArkoorDestination {
1667 total_amount: Amount::from_sat(99_600),
1668 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1669 },
1670 ];
1671
1672 let dust_outputs = vec![
1674 ArkoorDestination {
1675 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(charlie_keypair.public_key())
1677 },
1678 ArkoorDestination {
1679 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1681 }
1682 ];
1683
1684 let user_builder = ArkoorBuilder::new_with_checkpoint(
1685 alice_vtxo.clone(),
1686 outputs.clone(),
1687 dust_outputs.clone(),
1688 ).expect("Valid arkoor request with dust isolation");
1689
1690 verify_builder(&user_builder, &alice_vtxo, &outputs, &dust_outputs);
1691
1692 assert!(
1694 user_builder.unsigned_isolation_fanout_tx.is_some(),
1695 "Dust isolation should be active",
1696 );
1697
1698 assert_eq!(user_builder.nb_sigs(), 3);
1700
1701 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1702 let cosign_request = user_builder.cosign_request();
1703
1704 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1706 .expect("Invalid cosign request")
1707 .server_cosign(&server_keypair)
1708 .expect("Incorrect key");
1709
1710 let cosign_data = server_builder.cosign_response();
1711
1712 let vtxos = user_builder
1714 .user_cosign(&alice_keypair, &cosign_data)
1715 .expect("Valid cosign data and correct key")
1716 .build_signed_vtxos();
1717
1718 assert_eq!(vtxos.len(), 3);
1720
1721 for vtxo in vtxos.into_iter() {
1722 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1724
1725 let mut prev_tx = funding_tx.clone();
1727 for tx in vtxo.transactions().map(|item| item.tx) {
1728 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1729 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1730 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1731 prev_tx = tx;
1732 }
1733 }
1734 }
1735
1736 #[test]
1737 fn build_no_checkpoint_arkoor() {
1738 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1739 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1740 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1741
1742 println!("Alice keypair: {}", alice_keypair.public_key());
1743 println!("Bob keypair: {}", bob_keypair.public_key());
1744 println!("Server keypair: {}", server_keypair.public_key());
1745 println!("-----------------------------------------------");
1746
1747 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1748 amount: Amount::from_sat(100_330),
1749 fee: Amount::from_sat(330),
1750 expiry_height: 1000,
1751 exit_delta : 128,
1752 user_keypair: alice_keypair.clone(),
1753 server_keypair: server_keypair.clone()
1754 }.build();
1755
1756 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1758
1759 let dest = vec![
1760 ArkoorDestination {
1761 total_amount: Amount::from_sat(96_000),
1762 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1763 },
1764 ArkoorDestination {
1765 total_amount: Amount::from_sat(4_000),
1766 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1767 }
1768 ];
1769
1770 let user_builder = ArkoorBuilder::new_without_checkpoint(
1771 alice_vtxo.clone(),
1772 dest.clone(),
1773 vec![], ).expect("Valid arkoor request");
1775
1776 verify_builder(&user_builder, &alice_vtxo, &dest, &[]);
1777
1778 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1779 let cosign_request = user_builder.cosign_request();
1780
1781 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1783 .expect("Invalid cosign request")
1784 .server_cosign(&server_keypair)
1785 .expect("Incorrect key");
1786
1787 let cosign_data = server_builder.cosign_response();
1788
1789 let vtxos = user_builder
1791 .user_cosign(&alice_keypair, &cosign_data)
1792 .expect("Valid cosign data and correct key")
1793 .build_signed_vtxos();
1794
1795 for vtxo in vtxos.into_iter() {
1796 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1798
1799 let mut prev_tx = funding_tx.clone();
1801 for tx in vtxo.transactions().map(|item| item.tx) {
1802 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1803 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1804 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1805 prev_tx = tx;
1806 }
1807 }
1808
1809 }
1810
1811 #[test]
1812 fn build_no_checkpoint_arkoor_with_dust_isolation() {
1813 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1816 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1817 let charlie_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1818 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1819
1820 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1821 amount: Amount::from_sat(100_330),
1822 fee: Amount::from_sat(330),
1823 expiry_height: 1000,
1824 exit_delta : 128,
1825 user_keypair: alice_keypair.clone(),
1826 server_keypair: server_keypair.clone()
1827 }.build();
1828
1829 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1831
1832 let outputs = vec![
1834 ArkoorDestination {
1835 total_amount: Amount::from_sat(99_600),
1836 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1837 },
1838 ];
1839
1840 let dust_outputs = vec![
1842 ArkoorDestination {
1843 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(charlie_keypair.public_key())
1845 },
1846 ArkoorDestination {
1847 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1849 }
1850 ];
1851
1852 let user_builder = ArkoorBuilder::new_without_checkpoint(
1853 alice_vtxo.clone(),
1854 outputs.clone(),
1855 dust_outputs.clone(),
1856 ).expect("Valid arkoor request with dust isolation");
1857
1858 verify_builder(&user_builder, &alice_vtxo, &outputs, &dust_outputs);
1859
1860 assert!(
1862 user_builder.unsigned_isolation_fanout_tx.is_some(),
1863 "Dust isolation should be active",
1864 );
1865
1866 assert_eq!(user_builder.nb_sigs(), 2);
1869
1870 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1871 let cosign_request = user_builder.cosign_request();
1872
1873 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1875 .expect("Invalid cosign request")
1876 .server_cosign(&server_keypair)
1877 .expect("Incorrect key");
1878
1879 let cosign_data = server_builder.cosign_response();
1880
1881 let vtxos = user_builder
1883 .user_cosign(&alice_keypair, &cosign_data)
1884 .expect("Valid cosign data and correct key")
1885 .build_signed_vtxos();
1886
1887 assert_eq!(vtxos.len(), 3);
1889
1890 for vtxo in vtxos.into_iter() {
1891 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1893
1894 let mut prev_tx = funding_tx.clone();
1896 for tx in vtxo.transactions().map(|item| item.tx) {
1897 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1898 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1899 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1900 prev_tx = tx;
1901 }
1902 }
1903 }
1904
1905 #[test]
1906 fn build_checkpointed_arkoor_outputs_must_be_above_dust_if_mixed() {
1907 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1909 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1910 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1911
1912 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1913 amount: Amount::from_sat(1_330),
1914 fee: Amount::from_sat(330),
1915 expiry_height: 1000,
1916 exit_delta : 128,
1917 user_keypair: alice_keypair.clone(),
1918 server_keypair: server_keypair.clone()
1919 }.build();
1920
1921 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1922
1923 ArkoorBuilder::new_with_checkpoint(
1925 alice_vtxo.clone(),
1926 vec![
1927 ArkoorDestination {
1928 total_amount: Amount::from_sat(100), policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1930 }; 10
1931 ],
1932 vec![],
1933 ).unwrap();
1934
1935 let res_empty = ArkoorBuilder::new_with_checkpoint(
1937 alice_vtxo.clone(),
1938 vec![],
1939 vec![
1940 ArkoorDestination {
1941 total_amount: Amount::from_sat(100), policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1943 }; 10
1944 ],
1945 );
1946 match res_empty {
1947 Err(ArkoorConstructionError::NoOutputs) => {},
1948 _ => panic!("Expected NoOutputs error for empty outputs"),
1949 }
1950
1951 ArkoorBuilder::new_with_checkpoint(
1953 alice_vtxo.clone(),
1954 vec![
1955 ArkoorDestination {
1956 total_amount: Amount::from_sat(330), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1958 }; 2
1959 ],
1960 vec![
1961 ArkoorDestination {
1962 total_amount: Amount::from_sat(170),
1963 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1964 }; 2
1965 ],
1966 ).unwrap();
1967
1968 let res_mixed_small = ArkoorBuilder::new_with_checkpoint(
1970 alice_vtxo.clone(),
1971 vec![
1972 ArkoorDestination {
1973 total_amount: Amount::from_sat(500),
1974 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1975 },
1976 ArkoorDestination {
1977 total_amount: Amount::from_sat(300),
1978 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1979 }
1980 ],
1981 vec![
1982 ArkoorDestination {
1983 total_amount: Amount::from_sat(100),
1984 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1985 }; 2 ],
1987 );
1988 match res_mixed_small {
1989 Err(ArkoorConstructionError::Dust) => {},
1990 _ => panic!("Expected Dust error for isolation sum < 330"),
1991 }
1992 }
1993
1994 #[test]
1995 fn build_checkpointed_arkoor_dust_sum_too_small() {
1996 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1998 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1999 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2000
2001 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2002 amount: Amount::from_sat(100_330),
2003 fee: Amount::from_sat(330),
2004 expiry_height: 1000,
2005 exit_delta : 128,
2006 user_keypair: alice_keypair.clone(),
2007 server_keypair: server_keypair.clone()
2008 }.build();
2009
2010 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
2011
2012 let outputs = vec![
2014 ArkoorDestination {
2015 total_amount: Amount::from_sat(99_900),
2016 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2017 },
2018 ];
2019
2020 let dust_outputs = vec![
2022 ArkoorDestination {
2023 total_amount: Amount::from_sat(50),
2024 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2025 },
2026 ArkoorDestination {
2027 total_amount: Amount::from_sat(50),
2028 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2029 }
2030 ];
2031
2032 let result = ArkoorBuilder::new_with_checkpoint(
2034 alice_vtxo.clone(),
2035 outputs.clone(),
2036 dust_outputs.clone(),
2037 );
2038 match result {
2039 Err(ArkoorConstructionError::Dust) => {},
2040 _ => panic!("Expected Dust error for isolation sum < 330"),
2041 }
2042 }
2043
2044 #[test]
2045 fn spend_dust_vtxo() {
2046 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2048 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2049 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2050
2051 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2053 amount: Amount::from_sat(200),
2054 fee: Amount::ZERO,
2055 expiry_height: 1000,
2056 exit_delta: 128,
2057 user_keypair: alice_keypair.clone(),
2058 server_keypair: server_keypair.clone()
2059 }.build();
2060
2061 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
2062
2063 let dust_outputs = vec![
2066 ArkoorDestination {
2067 total_amount: Amount::from_sat(100),
2068 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2069 },
2070 ArkoorDestination {
2071 total_amount: Amount::from_sat(100),
2072 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2073 }
2074 ];
2075
2076 let user_builder = ArkoorBuilder::new_with_checkpoint(
2077 alice_vtxo.clone(),
2078 dust_outputs,
2079 vec![],
2080 ).expect("Valid arkoor request for all-dust case");
2081
2082 assert!(
2084 user_builder.unsigned_isolation_fanout_tx.is_none(),
2085 "Dust isolation should NOT be active",
2086 );
2087
2088 assert_eq!(user_builder.outputs.len(), 2);
2090
2091 assert_eq!(user_builder.nb_sigs(), 3);
2093
2094 let user_builder = user_builder.generate_user_nonces(alice_keypair);
2096 let cosign_request = user_builder.cosign_request();
2097
2098 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
2100 .expect("Invalid cosign request")
2101 .server_cosign(&server_keypair)
2102 .expect("Incorrect key");
2103
2104 let cosign_data = server_builder.cosign_response();
2105
2106 let vtxos = user_builder
2108 .user_cosign(&alice_keypair, &cosign_data)
2109 .expect("Valid cosign data and correct key")
2110 .build_signed_vtxos();
2111
2112 assert_eq!(vtxos.len(), 2);
2114
2115 for vtxo in vtxos.into_iter() {
2116 vtxo.validate(&funding_tx).expect("Invalid VTXO");
2118
2119 assert_eq!(vtxo.amount(), Amount::from_sat(100));
2121
2122 let mut prev_tx = funding_tx.clone();
2124 for tx in vtxo.transactions().map(|item| item.tx) {
2125 let prev_outpoint: OutPoint = tx.input[0].previous_output;
2126 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
2127 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
2128 prev_tx = tx;
2129 }
2130 }
2131 }
2132
2133 #[test]
2134 fn spend_nondust_vtxo_to_dust() {
2135 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2138 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2139 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2140
2141 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2143 amount: Amount::from_sat(500),
2144 fee: Amount::ZERO,
2145 expiry_height: 1000,
2146 exit_delta: 128,
2147 user_keypair: alice_keypair.clone(),
2148 server_keypair: server_keypair.clone()
2149 }.build();
2150
2151 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
2152
2153 let dust_outputs = vec![
2156 ArkoorDestination {
2157 total_amount: Amount::from_sat(250),
2158 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2159 },
2160 ArkoorDestination {
2161 total_amount: Amount::from_sat(250),
2162 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2163 }
2164 ];
2165
2166 let user_builder = ArkoorBuilder::new_with_checkpoint(
2167 alice_vtxo.clone(),
2168 dust_outputs,
2169 vec![],
2170 ).expect("Valid arkoor request for non-dust to dust case");
2171
2172 assert!(
2174 user_builder.unsigned_isolation_fanout_tx.is_none(),
2175 "Dust isolation should NOT be active",
2176 );
2177
2178 assert_eq!(user_builder.outputs.len(), 2);
2180
2181 assert_eq!(user_builder.nb_sigs(), 3);
2183
2184 let user_builder = user_builder.generate_user_nonces(alice_keypair);
2186 let cosign_request = user_builder.cosign_request();
2187
2188 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
2190 .expect("Invalid cosign request")
2191 .server_cosign(&server_keypair)
2192 .expect("Incorrect key");
2193
2194 let cosign_data = server_builder.cosign_response();
2195
2196 let vtxos = user_builder
2198 .user_cosign(&alice_keypair, &cosign_data)
2199 .expect("Valid cosign data and correct key")
2200 .build_signed_vtxos();
2201
2202 assert_eq!(vtxos.len(), 2);
2204
2205 for vtxo in vtxos.into_iter() {
2206 vtxo.validate(&funding_tx).expect("Invalid VTXO");
2208
2209 assert_eq!(vtxo.amount(), Amount::from_sat(250));
2211
2212 let mut prev_tx = funding_tx.clone();
2214 for tx in vtxo.transactions().map(|item| item.tx) {
2215 let prev_outpoint: OutPoint = tx.input[0].previous_output;
2216 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
2217 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
2218 prev_tx = tx;
2219 }
2220 }
2221 }
2222
2223 #[test]
2224 fn isolate_dust_all_nondust() {
2225 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2228 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2229 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2230
2231 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2232 amount: Amount::from_sat(1000),
2233 fee: Amount::ZERO,
2234 expiry_height: 1000,
2235 exit_delta: 128,
2236 user_keypair: alice_keypair.clone(),
2237 server_keypair: server_keypair.clone()
2238 }.build();
2239
2240 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2241
2242 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2243 alice_vtxo,
2244 vec![
2245 ArkoorDestination {
2246 total_amount: Amount::from_sat(500),
2247 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2248 },
2249 ArkoorDestination {
2250 total_amount: Amount::from_sat(500),
2251 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2252 }
2253 ],
2254 ).unwrap();
2255
2256 assert!(builder.unsigned_isolation_fanout_tx.is_none());
2258
2259 assert_eq!(builder.outputs.len(), 2);
2261 assert_eq!(builder.isolated_outputs.len(), 0);
2262 }
2263
2264 #[test]
2265 fn isolate_dust_all_dust() {
2266 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2269 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2270 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2271
2272 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2273 amount: Amount::from_sat(400),
2274 fee: Amount::ZERO,
2275 expiry_height: 1000,
2276 exit_delta: 128,
2277 user_keypair: alice_keypair.clone(),
2278 server_keypair: server_keypair.clone()
2279 }.build();
2280
2281 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2282
2283 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2284 alice_vtxo,
2285 vec![
2286 ArkoorDestination {
2287 total_amount: Amount::from_sat(200),
2288 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2289 },
2290 ArkoorDestination {
2291 total_amount: Amount::from_sat(200),
2292 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2293 }
2294 ],
2295 ).unwrap();
2296
2297 assert!(builder.unsigned_isolation_fanout_tx.is_none());
2299
2300 assert_eq!(builder.outputs.len(), 2);
2302 assert_eq!(builder.isolated_outputs.len(), 0);
2303 }
2304
2305 #[test]
2306 fn isolate_dust_sufficient_dust() {
2307 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2310 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2311 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2312
2313 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2314 amount: Amount::from_sat(1000),
2315 fee: Amount::ZERO,
2316 expiry_height: 1000,
2317 exit_delta: 128,
2318 user_keypair: alice_keypair.clone(),
2319 server_keypair: server_keypair.clone()
2320 }.build();
2321
2322 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2323
2324 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2326 alice_vtxo,
2327 vec![
2328 ArkoorDestination {
2329 total_amount: Amount::from_sat(600),
2330 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2331 },
2332 ArkoorDestination {
2333 total_amount: Amount::from_sat(200),
2334 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2335 },
2336 ArkoorDestination {
2337 total_amount: Amount::from_sat(200),
2338 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2339 }
2340 ],
2341 ).unwrap();
2342
2343 assert!(builder.unsigned_isolation_fanout_tx.is_some());
2345
2346 assert_eq!(builder.outputs.len(), 1);
2348 assert_eq!(builder.isolated_outputs.len(), 2);
2349 }
2350
2351 #[test]
2352 fn isolate_dust_split_successful() {
2353 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2357 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2358 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2359
2360 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2361 amount: Amount::from_sat(1000),
2362 fee: Amount::ZERO,
2363 expiry_height: 1000,
2364 exit_delta: 128,
2365 user_keypair: alice_keypair.clone(),
2366 server_keypair: server_keypair.clone()
2367 }.build();
2368
2369 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2370
2371 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2372 alice_vtxo,
2373 vec![
2374 ArkoorDestination {
2375 total_amount: Amount::from_sat(800),
2376 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2377 },
2378 ArkoorDestination {
2379 total_amount: Amount::from_sat(100),
2380 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2381 },
2382 ArkoorDestination {
2383 total_amount: Amount::from_sat(100),
2384 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2385 }
2386 ],
2387 ).unwrap();
2388
2389 assert!(builder.unsigned_isolation_fanout_tx.is_some());
2391
2392 assert_eq!(builder.outputs.len(), 1);
2394 assert_eq!(builder.isolated_outputs.len(), 3);
2395
2396 assert_eq!(builder.outputs[0].total_amount, Amount::from_sat(670));
2398 let isolated_sum: Amount = builder.isolated_outputs.iter().map(|o| o.total_amount).sum();
2399 assert_eq!(isolated_sum, P2TR_DUST);
2400 }
2401
2402 #[test]
2403 fn isolate_dust_split_impossible() {
2404 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2409 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2410 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2411
2412 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2413 amount: Amount::from_sat(600),
2414 fee: Amount::ZERO,
2415 expiry_height: 1000,
2416 exit_delta: 128,
2417 user_keypair: alice_keypair.clone(),
2418 server_keypair: server_keypair.clone()
2419 }.build();
2420
2421 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2422
2423 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2424 alice_vtxo,
2425 vec![
2426 ArkoorDestination {
2427 total_amount: Amount::from_sat(400),
2428 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2429 },
2430 ArkoorDestination {
2431 total_amount: Amount::from_sat(100),
2432 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2433 },
2434 ArkoorDestination {
2435 total_amount: Amount::from_sat(100),
2436 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2437 }
2438 ],
2439 ).unwrap();
2440
2441 assert!(builder.unsigned_isolation_fanout_tx.is_none());
2443
2444 assert_eq!(builder.outputs.len(), 3);
2446 assert_eq!(builder.isolated_outputs.len(), 0);
2447 }
2448
2449 #[test]
2450 fn isolate_dust_exactly_boundary() {
2451 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2455 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2456 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2457
2458 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2459 amount: Amount::from_sat(1000),
2460 fee: Amount::ZERO,
2461 expiry_height: 1000,
2462 exit_delta: 128,
2463 user_keypair: alice_keypair.clone(),
2464 server_keypair: server_keypair.clone()
2465 }.build();
2466
2467 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2468
2469 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2470 alice_vtxo,
2471 vec![
2472 ArkoorDestination {
2473 total_amount: Amount::from_sat(660),
2474 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2475 },
2476 ArkoorDestination {
2477 total_amount: Amount::from_sat(170),
2478 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2479 },
2480 ArkoorDestination {
2481 total_amount: Amount::from_sat(170),
2482 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2483 }
2484 ],
2485 ).unwrap();
2486
2487 assert!(builder.unsigned_isolation_fanout_tx.is_some());
2489
2490 assert_eq!(builder.outputs.len(), 1);
2492 assert_eq!(builder.isolated_outputs.len(), 2);
2493
2494 assert_eq!(builder.outputs[0].total_amount, Amount::from_sat(660));
2496 assert_eq!(builder.isolated_outputs[0].total_amount, Amount::from_sat(170));
2497 assert_eq!(builder.isolated_outputs[1].total_amount, Amount::from_sat(170));
2498 }
2499}