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