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].utxo(),
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<'a>(&'a self) -> impl Iterator<Item = ServerVtxo<Full>> + 'a {
699 let checkpoint_vtxos = {
700 let range = if self.checkpoint_data.is_some() {
701 0..self.outputs.len()
702 } else {
703 0..0
705 };
706 range.map(|i| self.build_checkpoint_vtxo_at(i, None))
707 };
708
709 let isolation_vtxo = if !self.isolated_outputs.is_empty() {
710 let output_idx = self.outputs.len();
712
713 let (int_tx, int_txid) = if let Some((tx, txid)) = &self.checkpoint_data {
715 (tx, *txid)
716 } else {
717 let arkoor_tx = &self.unsigned_arkoor_txs[0];
718 (arkoor_tx, arkoor_tx.compute_txid())
719 };
720
721 Some(Vtxo {
722 amount: self.isolated_outputs.iter().map(|o| o.total_amount).sum(),
723 policy: ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey()),
724 expiry_height: self.input.expiry_height,
725 server_pubkey: self.input.server_pubkey,
726 exit_delta: self.input.exit_delta,
727 point: OutPoint::new(int_txid, output_idx as u32),
728 anchor_point: self.input.anchor_point,
729 genesis: Full {
730 items: self.input.genesis.items.clone().into_iter().chain([
731 GenesisItem {
732 transition: GenesisTransition::new_arkoor(
733 vec![self.input.user_pubkey()],
734 self.input_tweak,
735 None,
736 ),
737 output_idx: output_idx as u8,
738 other_outputs: int_tx.output.iter().enumerate()
739 .filter_map(|(i, txout)| {
740 if i == output_idx || txout.is_p2a_fee_anchor() {
741 None
742 } else {
743 Some(txout.clone())
744 }
745 })
746 .collect(),
747 fee_amount: Amount::ZERO,
748 },
749 ]).collect(),
750 },
751 })
752 } else {
753 None
754 };
755
756 checkpoint_vtxos.chain(isolation_vtxo)
757 }
758
759 pub fn spend_info(&self) -> Vec<(VtxoId, Txid)> {
761 let mut ret = Vec::with_capacity(1 + self.outputs.len());
762
763 if let Some((_tx, checkpoint_txid)) = &self.checkpoint_data {
764 ret.push((self.input.id(), *checkpoint_txid));
766
767 for idx in 0..self.outputs.len() {
769 ret.push((
770 VtxoId::from(OutPoint::new(*checkpoint_txid, idx as u32)),
771 self.unsigned_arkoor_txs[idx].compute_txid()
772 ));
773 }
774
775 if let Some(fanout_tx) = &self.unsigned_isolation_fanout_tx {
777 let fanout_txid = fanout_tx.compute_txid();
778
779 let isolated_output_idx = self.outputs.len() as u32;
781 ret.push((
782 VtxoId::from(OutPoint::new(*checkpoint_txid, isolated_output_idx)),
783 fanout_txid,
784 ));
785 }
786 } else {
787 let arkoor_txid = self.unsigned_arkoor_txs[0].compute_txid();
788
789 ret.push((self.input.id(), arkoor_txid));
791
792 if let Some(fanout_tx) = &self.unsigned_isolation_fanout_tx {
794 let fanout_txid = fanout_tx.compute_txid();
795
796 let isolation_output_idx = self.outputs.len() as u32;
798 ret.push((
799 VtxoId::from(OutPoint::new(arkoor_txid, isolation_output_idx)),
800 fanout_txid,
801 ));
802 }
803 }
804
805 ret
806 }
807
808 pub fn virtual_transactions(&self) -> Vec<Txid> {
813 let mut ret = Vec::new();
814 if let Some((_, txid)) = &self.checkpoint_data {
816 ret.push(*txid);
817 }
818 ret.extend(self.unsigned_arkoor_txs.iter().map(|tx| tx.compute_txid()));
820 if let Some(tx) = &self.unsigned_isolation_fanout_tx {
822 ret.push(tx.compute_txid());
823 }
824 ret
825 }
826
827 fn taptweak_at(&self, idx: usize) -> TapTweakHash {
828 if idx == 0 { self.input_tweak } else { self.checkpoint_policy_tweak }
829 }
830
831 fn user_pubkey(&self) -> PublicKey {
832 self.input.user_pubkey()
833 }
834
835 fn server_pubkey(&self) -> PublicKey {
836 self.input.server_pubkey()
837 }
838
839 fn construct_unsigned_checkpoint_tx<G>(
844 input: &Vtxo<G>,
845 outputs: &[ArkoorDestination],
846 dust_isolation_amount: Option<Amount>,
847 ) -> Transaction {
848
849 let output_policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
851 let checkpoint_spk = output_policy
852 .script_pubkey(input.server_pubkey(), input.exit_delta(), input.expiry_height());
853
854 Transaction {
855 version: bitcoin::transaction::Version(3),
856 lock_time: bitcoin::absolute::LockTime::ZERO,
857 input: vec![TxIn {
858 previous_output: input.point(),
859 script_sig: ScriptBuf::new(),
860 sequence: Sequence::ZERO,
861 witness: Witness::new(),
862 }],
863 output: outputs.iter().map(|o| {
864 TxOut {
865 value: o.total_amount,
866 script_pubkey: checkpoint_spk.clone(),
867 }
868 })
869 .chain(dust_isolation_amount.map(|amt| {
871 TxOut {
872 value: amt,
873 script_pubkey: checkpoint_spk.clone(),
874 }
875 }))
876 .chain([fee::fee_anchor()]).collect()
877 }
878 }
879
880 fn construct_unsigned_arkoor_txs<G>(
881 input: &Vtxo<G>,
882 outputs: &[ArkoorDestination],
883 checkpoint_txid: Option<Txid>,
884 dust_isolation_amount: Option<Amount>,
885 ) -> Vec<Transaction> {
886
887 if let Some(checkpoint_txid) = checkpoint_txid {
888 let mut arkoor_txs = Vec::with_capacity(outputs.len());
890
891 for (vout, output) in outputs.iter().enumerate() {
892 let transaction = Transaction {
893 version: bitcoin::transaction::Version(3),
894 lock_time: bitcoin::absolute::LockTime::ZERO,
895 input: vec![TxIn {
896 previous_output: OutPoint::new(checkpoint_txid, vout as u32),
897 script_sig: ScriptBuf::new(),
898 sequence: Sequence::ZERO,
899 witness: Witness::new(),
900 }],
901 output: vec![
902 output.policy.txout(
903 output.total_amount,
904 input.server_pubkey(),
905 input.exit_delta(),
906 input.expiry_height(),
907 ),
908 fee::fee_anchor(),
909 ]
910 };
911 arkoor_txs.push(transaction);
912 }
913
914 arkoor_txs
915 } else {
916 let checkpoint_policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
918 let checkpoint_spk = checkpoint_policy.script_pubkey(
919 input.server_pubkey(),
920 input.exit_delta(),
921 input.expiry_height()
922 );
923
924 let transaction = Transaction {
925 version: bitcoin::transaction::Version(3),
926 lock_time: bitcoin::absolute::LockTime::ZERO,
927 input: vec![TxIn {
928 previous_output: input.point(),
929 script_sig: ScriptBuf::new(),
930 sequence: Sequence::ZERO,
931 witness: Witness::new(),
932 }],
933 output: outputs.iter()
934 .map(|o| o.policy.txout(
935 o.total_amount,
936 input.server_pubkey(),
937 input.exit_delta(),
938 input.expiry_height(),
939 ))
940 .chain(dust_isolation_amount.map(|amt| TxOut {
942 value: amt,
943 script_pubkey: checkpoint_spk.clone(),
944 }))
945 .chain([fee::fee_anchor()])
946 .collect()
947 };
948 vec![transaction]
949 }
950 }
951
952 fn construct_unsigned_isolation_fanout_tx<G>(
960 input: &Vtxo<G>,
961 isolated_outputs: &[ArkoorDestination],
962 parent_txid: Txid, dust_isolation_output_vout: u32, ) -> Transaction {
965
966 Transaction {
967 version: bitcoin::transaction::Version(3),
968 lock_time: bitcoin::absolute::LockTime::ZERO,
969 input: vec![TxIn {
970 previous_output: OutPoint::new(parent_txid, dust_isolation_output_vout),
971 script_sig: ScriptBuf::new(),
972 sequence: Sequence::ZERO,
973 witness: Witness::new(),
974 }],
975 output: isolated_outputs.iter().map(|o| {
976 TxOut {
977 value: o.total_amount,
978 script_pubkey: o.policy.script_pubkey(
979 input.server_pubkey(),
980 input.exit_delta(),
981 input.expiry_height(),
982 ),
983 }
984 }).chain([fee::fee_anchor()]).collect(),
985 }
986 }
987
988 fn validate_amounts<G>(
989 input: &Vtxo<G>,
990 outputs: &[ArkoorDestination],
991 isolation_outputs: &[ArkoorDestination],
992 ) -> Result<(), ArkoorConstructionError> {
993
994 let input_amount = input.amount();
999 let output_amount = outputs.iter().chain(isolation_outputs.iter())
1000 .map(|o| o.total_amount).sum::<Amount>();
1001
1002 if input_amount != output_amount {
1003 return Err(ArkoorConstructionError::Unbalanced {
1004 input: input_amount,
1005 output: output_amount,
1006 })
1007 }
1008
1009 if outputs.is_empty() {
1011 return Err(ArkoorConstructionError::NoOutputs)
1012 }
1013
1014 if !isolation_outputs.is_empty() {
1016 let isolation_sum: Amount = isolation_outputs.iter()
1017 .map(|o| o.total_amount).sum();
1018 if isolation_sum < P2TR_DUST {
1019 return Err(ArkoorConstructionError::Dust)
1020 }
1021 }
1022
1023 Ok(())
1024 }
1025
1026
1027 fn to_state<S2: state::BuilderState>(self) -> ArkoorBuilder<S2> {
1028 ArkoorBuilder {
1029 input: self.input,
1030 outputs: self.outputs,
1031 isolated_outputs: self.isolated_outputs,
1032 checkpoint_data: self.checkpoint_data,
1033 unsigned_arkoor_txs: self.unsigned_arkoor_txs,
1034 unsigned_isolation_fanout_tx: self.unsigned_isolation_fanout_tx,
1035 new_vtxo_ids: self.new_vtxo_ids,
1036 sighashes: self.sighashes,
1037 input_tweak: self.input_tweak,
1038 checkpoint_policy_tweak: self.checkpoint_policy_tweak,
1039 user_keypair: self.user_keypair,
1040 user_pub_nonces: self.user_pub_nonces,
1041 user_sec_nonces: self.user_sec_nonces,
1042 server_pub_nonces: self.server_pub_nonces,
1043 server_partial_sigs: self.server_partial_sigs,
1044 full_signatures: self.full_signatures,
1045 _state: PhantomData,
1046 }
1047 }
1048}
1049
1050impl ArkoorBuilder<state::Initial> {
1051 pub fn new_with_checkpoint(
1053 input: Vtxo<Full>,
1054 outputs: Vec<ArkoorDestination>,
1055 isolated_outputs: Vec<ArkoorDestination>,
1056 ) -> Result<Self, ArkoorConstructionError> {
1057 Self::new(input, outputs, isolated_outputs, true)
1058 }
1059
1060 pub fn new_without_checkpoint(
1062 input: Vtxo<Full>,
1063 outputs: Vec<ArkoorDestination>,
1064 isolated_outputs: Vec<ArkoorDestination>,
1065 ) -> Result<Self, ArkoorConstructionError> {
1066 Self::new(input, outputs, isolated_outputs, false)
1067 }
1068
1069 pub fn new_with_checkpoint_isolate_dust(
1074 input: Vtxo<Full>,
1075 outputs: Vec<ArkoorDestination>,
1076 ) -> Result<Self, ArkoorConstructionError> {
1077 Self::new_isolate_dust(input, outputs, true)
1078 }
1079
1080 pub(crate) fn new_isolate_dust(
1081 input: Vtxo<Full>,
1082 outputs: Vec<ArkoorDestination>,
1083 use_checkpoints: bool,
1084 ) -> Result<Self, ArkoorConstructionError> {
1085 if outputs.iter().all(|v| v.total_amount >= P2TR_DUST)
1087 || outputs.iter().all(|v| v.total_amount < P2TR_DUST)
1088 {
1089 return Self::new(input, outputs, vec![], use_checkpoints);
1090 }
1091
1092 let (mut dust, mut non_dust) = outputs.iter().cloned()
1094 .partition::<Vec<_>, _>(|v| v.total_amount < P2TR_DUST);
1095
1096 let dust_sum = dust.iter().map(|o| o.total_amount).sum::<Amount>();
1097 if dust_sum >= P2TR_DUST {
1098 return Self::new(input, non_dust, dust, use_checkpoints);
1099 }
1100
1101 let non_dust_sum = non_dust.iter().map(|o| o.total_amount).sum::<Amount>();
1103 if non_dust_sum < P2TR_DUST * 2 {
1104 return Self::new(input, outputs, vec![], use_checkpoints);
1105 }
1106
1107 let deficit = P2TR_DUST - dust_sum;
1109 let split_idx = non_dust.iter()
1112 .position(|o| o.total_amount - deficit >= P2TR_DUST);
1113
1114 if let Some(idx) = split_idx {
1115 let output_to_split = non_dust[idx].clone();
1116
1117 let dust_piece = ArkoorDestination {
1118 total_amount: deficit,
1119 policy: output_to_split.policy.clone(),
1120 };
1121 let leftover = ArkoorDestination {
1122 total_amount: output_to_split.total_amount - deficit,
1123 policy: output_to_split.policy,
1124 };
1125
1126 non_dust[idx] = leftover;
1127 dust.insert(0, dust_piece);
1129
1130 return Self::new(input, non_dust, dust, use_checkpoints);
1131 } else {
1132 let all_outputs = non_dust.into_iter().chain(dust).collect();
1134 return Self::new(input, all_outputs, vec![], use_checkpoints);
1135 }
1136 }
1137
1138 pub(crate) fn new(
1139 input: Vtxo<Full>,
1140 outputs: Vec<ArkoorDestination>,
1141 isolated_outputs: Vec<ArkoorDestination>,
1142 use_checkpoint: bool,
1143 ) -> Result<Self, ArkoorConstructionError> {
1144 Self::validate_amounts(&input, &outputs, &isolated_outputs)?;
1146
1147 let combined_dust_amount = if !isolated_outputs.is_empty() {
1149 Some(isolated_outputs.iter().map(|o| o.total_amount).sum())
1150 } else {
1151 None
1152 };
1153
1154 let unsigned_checkpoint_tx = if use_checkpoint {
1156 let tx = Self::construct_unsigned_checkpoint_tx(
1157 &input,
1158 &outputs,
1159 combined_dust_amount,
1160 );
1161 let txid = tx.compute_txid();
1162 Some((tx, txid))
1163 } else {
1164 None
1165 };
1166
1167 let unsigned_arkoor_txs = Self::construct_unsigned_arkoor_txs(
1169 &input,
1170 &outputs,
1171 unsigned_checkpoint_tx.as_ref().map(|t| t.1),
1172 combined_dust_amount,
1173 );
1174
1175 let unsigned_isolation_fanout_tx = if !isolated_outputs.is_empty() {
1177 let dust_isolation_output_vout = outputs.len() as u32;
1180
1181 let parent_txid = if let Some((_tx, txid)) = &unsigned_checkpoint_tx {
1182 *txid
1183 } else {
1184 unsigned_arkoor_txs[0].compute_txid()
1185 };
1186
1187 Some(Self::construct_unsigned_isolation_fanout_tx(
1188 &input,
1189 &isolated_outputs,
1190 parent_txid,
1191 dust_isolation_output_vout,
1192 ))
1193 } else {
1194 None
1195 };
1196
1197 let new_vtxo_ids = unsigned_arkoor_txs.iter()
1199 .map(|tx| OutPoint::new(tx.compute_txid(), 0))
1200 .map(|outpoint| VtxoId::from(outpoint))
1201 .collect();
1202
1203 let mut sighashes = Vec::new();
1205
1206 if let Some((checkpoint_tx, _txid)) = &unsigned_checkpoint_tx {
1207 sighashes.push(arkoor_sighash(&input.txout(), checkpoint_tx));
1209
1210 for vout in 0..outputs.len() {
1212 let prevout = checkpoint_tx.output[vout].clone();
1213 sighashes.push(arkoor_sighash(&prevout, &unsigned_arkoor_txs[vout]));
1214 }
1215 } else {
1216 sighashes.push(arkoor_sighash(&input.txout(), &unsigned_arkoor_txs[0]));
1218 }
1219
1220 if let Some(ref tx) = unsigned_isolation_fanout_tx {
1222 let dust_output_vout = outputs.len(); let prevout = if let Some((checkpoint_tx, _txid)) = &unsigned_checkpoint_tx {
1224 checkpoint_tx.output[dust_output_vout].clone()
1225 } else {
1226 unsigned_arkoor_txs[0].output[dust_output_vout].clone()
1228 };
1229 sighashes.push(arkoor_sighash(&prevout, tx));
1230 }
1231
1232 let policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
1234 let input_tweak = input.output_taproot().tap_tweak();
1235 let checkpoint_policy_tweak = policy.taproot(
1236 input.server_pubkey(),
1237 input.exit_delta(),
1238 input.expiry_height(),
1239 ).tap_tweak();
1240
1241 Ok(Self {
1242 input: input,
1243 outputs: outputs,
1244 isolated_outputs,
1245 sighashes: sighashes,
1246 input_tweak,
1247 checkpoint_policy_tweak,
1248 checkpoint_data: unsigned_checkpoint_tx,
1249 unsigned_arkoor_txs: unsigned_arkoor_txs,
1250 unsigned_isolation_fanout_tx,
1251 new_vtxo_ids: new_vtxo_ids,
1252 user_keypair: None,
1253 user_pub_nonces: None,
1254 user_sec_nonces: None,
1255 server_pub_nonces: None,
1256 server_partial_sigs: None,
1257 full_signatures: None,
1258 _state: PhantomData,
1259 })
1260 }
1261
1262 pub fn generate_user_nonces(
1265 mut self,
1266 user_keypair: Keypair,
1267 ) -> ArkoorBuilder<state::UserGeneratedNonces> {
1268 let mut user_pub_nonces = Vec::with_capacity(self.nb_sigs());
1269 let mut user_sec_nonces = Vec::with_capacity(self.nb_sigs());
1270
1271 for idx in 0..self.nb_sigs() {
1272 let sighash = &self.sighashes[idx].to_byte_array();
1273 let (sec_nonce, pub_nonce) = musig::nonce_pair_with_msg(&user_keypair, sighash);
1274
1275 user_pub_nonces.push(pub_nonce);
1276 user_sec_nonces.push(sec_nonce);
1277 }
1278
1279 self.user_keypair = Some(user_keypair);
1280 self.user_pub_nonces = Some(user_pub_nonces);
1281 self.user_sec_nonces = Some(user_sec_nonces);
1282
1283 self.to_state::<state::UserGeneratedNonces>()
1284 }
1285
1286 fn set_user_pub_nonces(
1293 mut self,
1294 user_pub_nonces: Vec<musig::PublicNonce>,
1295 ) -> Result<ArkoorBuilder<state::ServerCanCosign>, ArkoorSigningError> {
1296 if user_pub_nonces.len() != self.nb_sigs() {
1297 return Err(ArkoorSigningError::InvalidNbUserNonces {
1298 expected: self.nb_sigs(),
1299 got: user_pub_nonces.len()
1300 })
1301 }
1302
1303 self.user_pub_nonces = Some(user_pub_nonces);
1304 Ok(self.to_state::<state::ServerCanCosign>())
1305 }
1306}
1307
1308impl<'a> ArkoorBuilder<state::ServerCanCosign> {
1309 pub fn from_cosign_request(
1310 cosign_request: ArkoorCosignRequest<Vtxo<Full>>,
1311 ) -> Result<ArkoorBuilder<state::ServerCanCosign>, ArkoorSigningError> {
1312 cosign_request.verify_attestation()
1313 .map_err(ArkoorSigningError::InvalidAttestation)?;
1314
1315 let ret = ArkoorBuilder::new(
1316 cosign_request.input,
1317 cosign_request.outputs,
1318 cosign_request.isolated_outputs,
1319 cosign_request.use_checkpoint,
1320 )
1321 .map_err(ArkoorSigningError::ArkoorConstructionError)?
1322 .set_user_pub_nonces(cosign_request.user_pub_nonces.clone())?;
1323 Ok(ret)
1324 }
1325
1326 pub fn server_cosign(
1327 mut self,
1328 server_keypair: &Keypair,
1329 ) -> Result<ArkoorBuilder<state::ServerSigned>, ArkoorSigningError> {
1330 if server_keypair.public_key() != self.input.server_pubkey() {
1332 return Err(ArkoorSigningError::IncorrectKey {
1333 expected: self.input.server_pubkey(),
1334 got: server_keypair.public_key(),
1335 });
1336 }
1337
1338 let mut server_pub_nonces = Vec::with_capacity(self.outputs.len() + 1);
1339 let mut server_partial_sigs = Vec::with_capacity(self.outputs.len() + 1);
1340
1341 for idx in 0..self.nb_sigs() {
1342 let (server_pub_nonce, server_partial_sig) = musig::deterministic_partial_sign(
1343 &server_keypair,
1344 [self.input.user_pubkey()],
1345 &[&self.user_pub_nonces.as_ref().expect("state-invariant")[idx]],
1346 self.sighashes[idx].to_byte_array(),
1347 Some(self.taptweak_at(idx).to_byte_array()),
1348 );
1349
1350 server_pub_nonces.push(server_pub_nonce);
1351 server_partial_sigs.push(server_partial_sig);
1352 };
1353
1354 self.server_pub_nonces = Some(server_pub_nonces);
1355 self.server_partial_sigs = Some(server_partial_sigs);
1356 Ok(self.to_state::<state::ServerSigned>())
1357 }
1358}
1359
1360impl ArkoorBuilder<state::ServerSigned> {
1361 pub fn user_pub_nonces(&self) -> Vec<musig::PublicNonce> {
1362 self.user_pub_nonces.as_ref().expect("state invariant").clone()
1363 }
1364
1365 pub fn server_partial_signatures(&self) -> Vec<musig::PartialSignature> {
1366 self.server_partial_sigs.as_ref().expect("state invariant").clone()
1367 }
1368
1369 pub fn cosign_response(&self) -> ArkoorCosignResponse {
1370 ArkoorCosignResponse {
1371 server_pub_nonces: self.server_pub_nonces.as_ref()
1372 .expect("state invariant").clone(),
1373 server_partial_sigs: self.server_partial_sigs.as_ref()
1374 .expect("state invariant").clone(),
1375 }
1376 }
1377}
1378
1379impl ArkoorBuilder<state::UserGeneratedNonces> {
1380 pub fn user_pub_nonces(&self) -> &[PublicNonce] {
1381 self.user_pub_nonces.as_ref().expect("State invariant")
1382 }
1383
1384 pub fn cosign_request(&self) -> ArkoorCosignRequest<Vtxo<Full>> {
1385 ArkoorCosignRequest::new(
1386 self.user_pub_nonces().to_vec(),
1387 self.input.clone(),
1388 self.outputs.clone(),
1389 self.isolated_outputs.clone(),
1390 self.checkpoint_data.is_some(),
1391 self.user_keypair.as_ref().expect("State invariant"),
1392 )
1393 }
1394
1395 fn validate_server_cosign_response(
1396 &self,
1397 data: &ArkoorCosignResponse,
1398 ) -> Result<(), ArkoorSigningError> {
1399
1400 if data.server_pub_nonces.len() != self.nb_sigs() {
1402 return Err(ArkoorSigningError::InvalidNbServerNonces {
1403 expected: self.nb_sigs(),
1404 got: data.server_pub_nonces.len(),
1405 });
1406 }
1407
1408 if data.server_partial_sigs.len() != self.nb_sigs() {
1409 return Err(ArkoorSigningError::InvalidNbServerPartialSigs {
1410 expected: self.nb_sigs(),
1411 got: data.server_partial_sigs.len(),
1412 })
1413 }
1414
1415 for idx in 0..self.nb_sigs() {
1417 let is_valid_sig = scripts::verify_partial_sig(
1418 self.sighashes[idx],
1419 self.taptweak_at(idx),
1420 (self.input.server_pubkey(), &data.server_pub_nonces[idx]),
1421 (self.input.user_pubkey(), &self.user_pub_nonces()[idx]),
1422 &data.server_partial_sigs[idx]
1423 );
1424
1425 if !is_valid_sig {
1426 return Err(ArkoorSigningError::InvalidPartialSignature {
1427 index: idx,
1428 });
1429 }
1430 }
1431 Ok(())
1432 }
1433
1434 pub fn user_cosign(
1435 mut self,
1436 user_keypair: &Keypair,
1437 server_cosign_data: &ArkoorCosignResponse,
1438 ) -> Result<ArkoorBuilder<state::UserSigned>, ArkoorSigningError> {
1439 if user_keypair.public_key() != self.input.user_pubkey() {
1441 return Err(ArkoorSigningError::IncorrectKey {
1442 expected: self.input.user_pubkey(),
1443 got: user_keypair.public_key(),
1444 });
1445 }
1446
1447 self.validate_server_cosign_response(&server_cosign_data)?;
1449
1450 let mut sigs = Vec::with_capacity(self.nb_sigs());
1451
1452 let user_sec_nonces = self.user_sec_nonces.take().expect("state invariant");
1455
1456 for (idx, user_sec_nonce) in user_sec_nonces.into_iter().enumerate() {
1457 let user_pub_nonce = self.user_pub_nonces()[idx];
1458 let server_pub_nonce = server_cosign_data.server_pub_nonces[idx];
1459 let agg_nonce = musig::nonce_agg(&[&user_pub_nonce, &server_pub_nonce]);
1460
1461 let (_partial, maybe_sig) = musig::partial_sign(
1462 [self.user_pubkey(), self.server_pubkey()],
1463 agg_nonce,
1464 &user_keypair,
1465 user_sec_nonce,
1466 self.sighashes[idx].to_byte_array(),
1467 Some(self.taptweak_at(idx).to_byte_array()),
1468 Some(&[&server_cosign_data.server_partial_sigs[idx]])
1469 );
1470
1471 let sig = maybe_sig.expect("The full signature exists. The server did sign first");
1472 sigs.push(sig);
1473 }
1474
1475 self.full_signatures = Some(sigs);
1476
1477 Ok(self.to_state::<state::UserSigned>())
1478 }
1479}
1480
1481
1482impl<'a> ArkoorBuilder<state::UserSigned> {
1483 pub fn build_signed_vtxos(&self) -> Vec<Vtxo<Full>> {
1484 let sigs = self.full_signatures.as_ref().expect("state invariant");
1485 let mut ret = Vec::with_capacity(self.outputs.len() + self.isolated_outputs.len());
1486
1487 if self.checkpoint_data.is_some() {
1488 let checkpoint_sig = sigs[0];
1489
1490 for i in 0..self.outputs.len() {
1492 let arkoor_sig = sigs[1 + i];
1493 ret.push(self.build_vtxo_at(i, Some(checkpoint_sig), Some(arkoor_sig)));
1494 }
1495
1496 if self.unsigned_isolation_fanout_tx.is_some() {
1498 let m = self.outputs.len();
1499 let fanout_tx_sig = sigs[1 + m];
1500
1501 for i in 0..self.isolated_outputs.len() {
1502 ret.push(self.build_isolated_vtxo_at(
1503 i,
1504 Some(checkpoint_sig),
1505 Some(fanout_tx_sig),
1506 ));
1507 }
1508 }
1509 } else {
1510 let arkoor_sig = sigs[0];
1512
1513 for i in 0..self.outputs.len() {
1515 ret.push(self.build_vtxo_at(i, None, Some(arkoor_sig)));
1516 }
1517
1518 if self.unsigned_isolation_fanout_tx.is_some() {
1520 let fanout_tx_sig = sigs[1];
1521
1522 for i in 0..self.isolated_outputs.len() {
1523 ret.push(self.build_isolated_vtxo_at(
1524 i,
1525 Some(arkoor_sig), Some(fanout_tx_sig),
1527 ));
1528 }
1529 }
1530 }
1531
1532 ret
1533 }
1534}
1535
1536fn arkoor_sighash(prevout: &TxOut, arkoor_tx: &Transaction) -> TapSighash {
1537 let mut shc = SighashCache::new(arkoor_tx);
1538
1539 shc.taproot_key_spend_signature_hash(
1540 0, &sighash::Prevouts::All(&[prevout]), TapSighashType::Default,
1541 ).expect("sighash error")
1542}
1543
1544#[cfg(test)]
1545mod test {
1546 use super::*;
1547
1548 use std::collections::HashSet;
1549
1550 use bitcoin::Amount;
1551 use bitcoin::secp256k1::Keypair;
1552 use bitcoin::secp256k1::rand;
1553
1554 use crate::SECP;
1555 use crate::test_util::dummy::DummyTestVtxoSpec;
1556 use crate::vtxo::VtxoId;
1557
1558 fn verify_builder<S: state::BuilderState>(
1560 builder: &ArkoorBuilder<S>,
1561 input: &Vtxo<Full>,
1562 outputs: &[ArkoorDestination],
1563 isolated_outputs: &[ArkoorDestination],
1564 ) {
1565 let has_isolation = !isolated_outputs.is_empty();
1566
1567 let spend_info = builder.spend_info();
1568 let spend_vtxo_ids: HashSet<VtxoId> = spend_info.iter().map(|(id, _)| *id).collect();
1569
1570 assert_eq!(spend_info[0].0, input.id());
1572
1573 assert_eq!(spend_vtxo_ids.len(), spend_info.len());
1575
1576 let internal_vtxos = builder.build_unsigned_internal_vtxos().collect::<Vec<_>>();
1578 let internal_vtxo_ids = internal_vtxos.iter().map(|v| v.id()).collect::<HashSet<_>>();
1579 for internal_vtxo in &internal_vtxos {
1580 assert!(spend_vtxo_ids.contains(&internal_vtxo.id()));
1581 assert!(matches!(internal_vtxo.policy(), ServerVtxoPolicy::Checkpoint(_)));
1582 }
1583
1584 for (vtxo_id, _) in &spend_info[1..] {
1586 assert!(internal_vtxo_ids.contains(vtxo_id));
1587 }
1588
1589 if has_isolation {
1591 let isolation_vtxo = internal_vtxos.last().unwrap();
1592 let expected_isolation_amount: Amount = isolated_outputs.iter()
1593 .map(|o| o.total_amount)
1594 .sum();
1595 assert_eq!(isolation_vtxo.amount(), expected_isolation_amount);
1596 }
1597
1598 let final_vtxos = builder.build_unsigned_vtxos().collect::<Vec<_>>();
1600 for final_vtxo in &final_vtxos {
1601 assert!(!spend_vtxo_ids.contains(&final_vtxo.id()));
1602 }
1603
1604 let all_destinations = outputs.iter()
1606 .chain(isolated_outputs.iter())
1607 .collect::<Vec<&_>>();
1608 for (vtxo, dest) in final_vtxos.iter().zip(all_destinations.iter()) {
1609 assert_eq!(vtxo.amount(), dest.total_amount);
1610 assert_eq!(vtxo.policy, dest.policy);
1611 }
1612
1613 let total_output_amount: Amount = final_vtxos.iter().map(|v| v.amount()).sum();
1615 assert_eq!(total_output_amount, input.amount());
1616 }
1617
1618 #[test]
1619 fn build_checkpointed_arkoor() {
1620 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1621 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1622 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1623
1624 println!("Alice keypair: {}", alice_keypair.public_key());
1625 println!("Bob keypair: {}", bob_keypair.public_key());
1626 println!("Server keypair: {}", server_keypair.public_key());
1627 println!("-----------------------------------------------");
1628
1629 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1630 amount: Amount::from_sat(100_330),
1631 fee: Amount::from_sat(330),
1632 expiry_height: 1000,
1633 exit_delta : 128,
1634 user_keypair: alice_keypair.clone(),
1635 server_keypair: server_keypair.clone()
1636 }.build();
1637
1638 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1640
1641 let dest = vec![
1642 ArkoorDestination {
1643 total_amount: Amount::from_sat(96_000),
1644 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1645 },
1646 ArkoorDestination {
1647 total_amount: Amount::from_sat(4_000),
1648 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1649 }
1650 ];
1651
1652 let user_builder = ArkoorBuilder::new_with_checkpoint(
1653 alice_vtxo.clone(),
1654 dest.clone(),
1655 vec![], ).expect("Valid arkoor request");
1657
1658 verify_builder(&user_builder, &alice_vtxo, &dest, &[]);
1659
1660 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1661 let cosign_request = user_builder.cosign_request();
1662
1663 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1665 .expect("Invalid cosign request")
1666 .server_cosign(&server_keypair)
1667 .expect("Incorrect key");
1668
1669 let cosign_data = server_builder.cosign_response();
1670
1671 let vtxos = user_builder
1673 .user_cosign(&alice_keypair, &cosign_data)
1674 .expect("Valid cosign data and correct key")
1675 .build_signed_vtxos();
1676
1677 for vtxo in vtxos.into_iter() {
1678 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1680
1681 let mut prev_tx = funding_tx.clone();
1683 for tx in vtxo.transactions().map(|item| item.tx) {
1684 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1685 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1686 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1687 prev_tx = tx;
1688 }
1689 }
1690
1691 }
1692
1693 #[test]
1694 fn build_checkpointed_arkoor_with_dust_isolation() {
1695 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1698 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1699 let charlie_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1700 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1701
1702 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1703 amount: Amount::from_sat(100_330),
1704 fee: Amount::from_sat(330),
1705 expiry_height: 1000,
1706 exit_delta : 128,
1707 user_keypair: alice_keypair.clone(),
1708 server_keypair: server_keypair.clone()
1709 }.build();
1710
1711 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1713
1714 let outputs = vec![
1716 ArkoorDestination {
1717 total_amount: Amount::from_sat(99_600),
1718 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1719 },
1720 ];
1721
1722 let dust_outputs = vec![
1724 ArkoorDestination {
1725 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(charlie_keypair.public_key())
1727 },
1728 ArkoorDestination {
1729 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1731 }
1732 ];
1733
1734 let user_builder = ArkoorBuilder::new_with_checkpoint(
1735 alice_vtxo.clone(),
1736 outputs.clone(),
1737 dust_outputs.clone(),
1738 ).expect("Valid arkoor request with dust isolation");
1739
1740 verify_builder(&user_builder, &alice_vtxo, &outputs, &dust_outputs);
1741
1742 assert!(
1744 user_builder.unsigned_isolation_fanout_tx.is_some(),
1745 "Dust isolation should be active",
1746 );
1747
1748 assert_eq!(user_builder.nb_sigs(), 3);
1750
1751 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1752 let cosign_request = user_builder.cosign_request();
1753
1754 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1756 .expect("Invalid cosign request")
1757 .server_cosign(&server_keypair)
1758 .expect("Incorrect key");
1759
1760 let cosign_data = server_builder.cosign_response();
1761
1762 let vtxos = user_builder
1764 .user_cosign(&alice_keypair, &cosign_data)
1765 .expect("Valid cosign data and correct key")
1766 .build_signed_vtxos();
1767
1768 assert_eq!(vtxos.len(), 3);
1770
1771 for vtxo in vtxos.into_iter() {
1772 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1774
1775 let mut prev_tx = funding_tx.clone();
1777 for tx in vtxo.transactions().map(|item| item.tx) {
1778 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1779 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1780 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1781 prev_tx = tx;
1782 }
1783 }
1784 }
1785
1786 #[test]
1787 fn build_no_checkpoint_arkoor() {
1788 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1789 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1790 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1791
1792 println!("Alice keypair: {}", alice_keypair.public_key());
1793 println!("Bob keypair: {}", bob_keypair.public_key());
1794 println!("Server keypair: {}", server_keypair.public_key());
1795 println!("-----------------------------------------------");
1796
1797 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1798 amount: Amount::from_sat(100_330),
1799 fee: Amount::from_sat(330),
1800 expiry_height: 1000,
1801 exit_delta : 128,
1802 user_keypair: alice_keypair.clone(),
1803 server_keypair: server_keypair.clone()
1804 }.build();
1805
1806 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1808
1809 let dest = vec![
1810 ArkoorDestination {
1811 total_amount: Amount::from_sat(96_000),
1812 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1813 },
1814 ArkoorDestination {
1815 total_amount: Amount::from_sat(4_000),
1816 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1817 }
1818 ];
1819
1820 let user_builder = ArkoorBuilder::new_without_checkpoint(
1821 alice_vtxo.clone(),
1822 dest.clone(),
1823 vec![], ).expect("Valid arkoor request");
1825
1826 verify_builder(&user_builder, &alice_vtxo, &dest, &[]);
1827
1828 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1829 let cosign_request = user_builder.cosign_request();
1830
1831 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1833 .expect("Invalid cosign request")
1834 .server_cosign(&server_keypair)
1835 .expect("Incorrect key");
1836
1837 let cosign_data = server_builder.cosign_response();
1838
1839 let vtxos = user_builder
1841 .user_cosign(&alice_keypair, &cosign_data)
1842 .expect("Valid cosign data and correct key")
1843 .build_signed_vtxos();
1844
1845 for vtxo in vtxos.into_iter() {
1846 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1848
1849 let mut prev_tx = funding_tx.clone();
1851 for tx in vtxo.transactions().map(|item| item.tx) {
1852 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1853 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1854 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1855 prev_tx = tx;
1856 }
1857 }
1858
1859 }
1860
1861 #[test]
1862 fn build_no_checkpoint_arkoor_with_dust_isolation() {
1863 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1866 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1867 let charlie_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1868 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1869
1870 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1871 amount: Amount::from_sat(100_330),
1872 fee: Amount::from_sat(330),
1873 expiry_height: 1000,
1874 exit_delta : 128,
1875 user_keypair: alice_keypair.clone(),
1876 server_keypair: server_keypair.clone()
1877 }.build();
1878
1879 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1881
1882 let outputs = vec![
1884 ArkoorDestination {
1885 total_amount: Amount::from_sat(99_600),
1886 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1887 },
1888 ];
1889
1890 let dust_outputs = vec![
1892 ArkoorDestination {
1893 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(charlie_keypair.public_key())
1895 },
1896 ArkoorDestination {
1897 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1899 }
1900 ];
1901
1902 let user_builder = ArkoorBuilder::new_without_checkpoint(
1903 alice_vtxo.clone(),
1904 outputs.clone(),
1905 dust_outputs.clone(),
1906 ).expect("Valid arkoor request with dust isolation");
1907
1908 verify_builder(&user_builder, &alice_vtxo, &outputs, &dust_outputs);
1909
1910 assert!(
1912 user_builder.unsigned_isolation_fanout_tx.is_some(),
1913 "Dust isolation should be active",
1914 );
1915
1916 assert_eq!(user_builder.nb_sigs(), 2);
1919
1920 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1921 let cosign_request = user_builder.cosign_request();
1922
1923 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1925 .expect("Invalid cosign request")
1926 .server_cosign(&server_keypair)
1927 .expect("Incorrect key");
1928
1929 let cosign_data = server_builder.cosign_response();
1930
1931 let vtxos = user_builder
1933 .user_cosign(&alice_keypair, &cosign_data)
1934 .expect("Valid cosign data and correct key")
1935 .build_signed_vtxos();
1936
1937 assert_eq!(vtxos.len(), 3);
1939
1940 for vtxo in vtxos.into_iter() {
1941 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1943
1944 let mut prev_tx = funding_tx.clone();
1946 for tx in vtxo.transactions().map(|item| item.tx) {
1947 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1948 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1949 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1950 prev_tx = tx;
1951 }
1952 }
1953 }
1954
1955 #[test]
1956 fn build_checkpointed_arkoor_outputs_must_be_above_dust_if_mixed() {
1957 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1959 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1960 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1961
1962 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1963 amount: Amount::from_sat(1_330),
1964 fee: Amount::from_sat(330),
1965 expiry_height: 1000,
1966 exit_delta : 128,
1967 user_keypair: alice_keypair.clone(),
1968 server_keypair: server_keypair.clone()
1969 }.build();
1970
1971 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1972
1973 ArkoorBuilder::new_with_checkpoint(
1975 alice_vtxo.clone(),
1976 vec![
1977 ArkoorDestination {
1978 total_amount: Amount::from_sat(100), policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1980 }; 10
1981 ],
1982 vec![],
1983 ).unwrap();
1984
1985 let res_empty = ArkoorBuilder::new_with_checkpoint(
1987 alice_vtxo.clone(),
1988 vec![],
1989 vec![
1990 ArkoorDestination {
1991 total_amount: Amount::from_sat(100), policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1993 }; 10
1994 ],
1995 );
1996 match res_empty {
1997 Err(ArkoorConstructionError::NoOutputs) => {},
1998 _ => panic!("Expected NoOutputs error for empty outputs"),
1999 }
2000
2001 ArkoorBuilder::new_with_checkpoint(
2003 alice_vtxo.clone(),
2004 vec![
2005 ArkoorDestination {
2006 total_amount: Amount::from_sat(330), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2008 }; 2
2009 ],
2010 vec![
2011 ArkoorDestination {
2012 total_amount: Amount::from_sat(170),
2013 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2014 }; 2
2015 ],
2016 ).unwrap();
2017
2018 let res_mixed_small = ArkoorBuilder::new_with_checkpoint(
2020 alice_vtxo.clone(),
2021 vec![
2022 ArkoorDestination {
2023 total_amount: Amount::from_sat(500),
2024 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2025 },
2026 ArkoorDestination {
2027 total_amount: Amount::from_sat(300),
2028 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2029 }
2030 ],
2031 vec![
2032 ArkoorDestination {
2033 total_amount: Amount::from_sat(100),
2034 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2035 }; 2 ],
2037 );
2038 match res_mixed_small {
2039 Err(ArkoorConstructionError::Dust) => {},
2040 _ => panic!("Expected Dust error for isolation sum < 330"),
2041 }
2042 }
2043
2044 #[test]
2045 fn build_checkpointed_arkoor_dust_sum_too_small() {
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 {
2052 amount: Amount::from_sat(100_330),
2053 fee: Amount::from_sat(330),
2054 expiry_height: 1000,
2055 exit_delta : 128,
2056 user_keypair: alice_keypair.clone(),
2057 server_keypair: server_keypair.clone()
2058 }.build();
2059
2060 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
2061
2062 let outputs = vec![
2064 ArkoorDestination {
2065 total_amount: Amount::from_sat(99_900),
2066 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2067 },
2068 ];
2069
2070 let dust_outputs = vec![
2072 ArkoorDestination {
2073 total_amount: Amount::from_sat(50),
2074 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2075 },
2076 ArkoorDestination {
2077 total_amount: Amount::from_sat(50),
2078 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2079 }
2080 ];
2081
2082 let result = ArkoorBuilder::new_with_checkpoint(
2084 alice_vtxo.clone(),
2085 outputs.clone(),
2086 dust_outputs.clone(),
2087 );
2088 match result {
2089 Err(ArkoorConstructionError::Dust) => {},
2090 _ => panic!("Expected Dust error for isolation sum < 330"),
2091 }
2092 }
2093
2094 #[test]
2095 fn spend_dust_vtxo() {
2096 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2098 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2099 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2100
2101 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2103 amount: Amount::from_sat(200),
2104 fee: Amount::ZERO,
2105 expiry_height: 1000,
2106 exit_delta: 128,
2107 user_keypair: alice_keypair.clone(),
2108 server_keypair: server_keypair.clone()
2109 }.build();
2110
2111 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
2112
2113 let dust_outputs = vec![
2116 ArkoorDestination {
2117 total_amount: Amount::from_sat(100),
2118 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2119 },
2120 ArkoorDestination {
2121 total_amount: Amount::from_sat(100),
2122 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2123 }
2124 ];
2125
2126 let user_builder = ArkoorBuilder::new_with_checkpoint(
2127 alice_vtxo.clone(),
2128 dust_outputs,
2129 vec![],
2130 ).expect("Valid arkoor request for all-dust case");
2131
2132 assert!(
2134 user_builder.unsigned_isolation_fanout_tx.is_none(),
2135 "Dust isolation should NOT be active",
2136 );
2137
2138 assert_eq!(user_builder.outputs.len(), 2);
2140
2141 assert_eq!(user_builder.nb_sigs(), 3);
2143
2144 let user_builder = user_builder.generate_user_nonces(alice_keypair);
2146 let cosign_request = user_builder.cosign_request();
2147
2148 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
2150 .expect("Invalid cosign request")
2151 .server_cosign(&server_keypair)
2152 .expect("Incorrect key");
2153
2154 let cosign_data = server_builder.cosign_response();
2155
2156 let vtxos = user_builder
2158 .user_cosign(&alice_keypair, &cosign_data)
2159 .expect("Valid cosign data and correct key")
2160 .build_signed_vtxos();
2161
2162 assert_eq!(vtxos.len(), 2);
2164
2165 for vtxo in vtxos.into_iter() {
2166 vtxo.validate(&funding_tx).expect("Invalid VTXO");
2168
2169 assert_eq!(vtxo.amount(), Amount::from_sat(100));
2171
2172 let mut prev_tx = funding_tx.clone();
2174 for tx in vtxo.transactions().map(|item| item.tx) {
2175 let prev_outpoint: OutPoint = tx.input[0].previous_output;
2176 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
2177 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
2178 prev_tx = tx;
2179 }
2180 }
2181 }
2182
2183 #[test]
2184 fn spend_nondust_vtxo_to_dust() {
2185 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2188 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2189 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2190
2191 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2193 amount: Amount::from_sat(500),
2194 fee: Amount::ZERO,
2195 expiry_height: 1000,
2196 exit_delta: 128,
2197 user_keypair: alice_keypair.clone(),
2198 server_keypair: server_keypair.clone()
2199 }.build();
2200
2201 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
2202
2203 let dust_outputs = vec![
2206 ArkoorDestination {
2207 total_amount: Amount::from_sat(250),
2208 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2209 },
2210 ArkoorDestination {
2211 total_amount: Amount::from_sat(250),
2212 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2213 }
2214 ];
2215
2216 let user_builder = ArkoorBuilder::new_with_checkpoint(
2217 alice_vtxo.clone(),
2218 dust_outputs,
2219 vec![],
2220 ).expect("Valid arkoor request for non-dust to dust case");
2221
2222 assert!(
2224 user_builder.unsigned_isolation_fanout_tx.is_none(),
2225 "Dust isolation should NOT be active",
2226 );
2227
2228 assert_eq!(user_builder.outputs.len(), 2);
2230
2231 assert_eq!(user_builder.nb_sigs(), 3);
2233
2234 let user_builder = user_builder.generate_user_nonces(alice_keypair);
2236 let cosign_request = user_builder.cosign_request();
2237
2238 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
2240 .expect("Invalid cosign request")
2241 .server_cosign(&server_keypair)
2242 .expect("Incorrect key");
2243
2244 let cosign_data = server_builder.cosign_response();
2245
2246 let vtxos = user_builder
2248 .user_cosign(&alice_keypair, &cosign_data)
2249 .expect("Valid cosign data and correct key")
2250 .build_signed_vtxos();
2251
2252 assert_eq!(vtxos.len(), 2);
2254
2255 for vtxo in vtxos.into_iter() {
2256 vtxo.validate(&funding_tx).expect("Invalid VTXO");
2258
2259 assert_eq!(vtxo.amount(), Amount::from_sat(250));
2261
2262 let mut prev_tx = funding_tx.clone();
2264 for tx in vtxo.transactions().map(|item| item.tx) {
2265 let prev_outpoint: OutPoint = tx.input[0].previous_output;
2266 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
2267 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
2268 prev_tx = tx;
2269 }
2270 }
2271 }
2272
2273 #[test]
2274 fn isolate_dust_all_nondust() {
2275 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2278 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2279 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2280
2281 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2282 amount: Amount::from_sat(1000),
2283 fee: Amount::ZERO,
2284 expiry_height: 1000,
2285 exit_delta: 128,
2286 user_keypair: alice_keypair.clone(),
2287 server_keypair: server_keypair.clone()
2288 }.build();
2289
2290 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2291
2292 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2293 alice_vtxo,
2294 vec![
2295 ArkoorDestination {
2296 total_amount: Amount::from_sat(500),
2297 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2298 },
2299 ArkoorDestination {
2300 total_amount: Amount::from_sat(500),
2301 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2302 }
2303 ],
2304 ).unwrap();
2305
2306 assert!(builder.unsigned_isolation_fanout_tx.is_none());
2308
2309 assert_eq!(builder.outputs.len(), 2);
2311 assert_eq!(builder.isolated_outputs.len(), 0);
2312 }
2313
2314 #[test]
2315 fn isolate_dust_all_dust() {
2316 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2319 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2320 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2321
2322 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2323 amount: Amount::from_sat(400),
2324 fee: Amount::ZERO,
2325 expiry_height: 1000,
2326 exit_delta: 128,
2327 user_keypair: alice_keypair.clone(),
2328 server_keypair: server_keypair.clone()
2329 }.build();
2330
2331 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2332
2333 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2334 alice_vtxo,
2335 vec![
2336 ArkoorDestination {
2337 total_amount: Amount::from_sat(200),
2338 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2339 },
2340 ArkoorDestination {
2341 total_amount: Amount::from_sat(200),
2342 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2343 }
2344 ],
2345 ).unwrap();
2346
2347 assert!(builder.unsigned_isolation_fanout_tx.is_none());
2349
2350 assert_eq!(builder.outputs.len(), 2);
2352 assert_eq!(builder.isolated_outputs.len(), 0);
2353 }
2354
2355 #[test]
2356 fn isolate_dust_sufficient_dust() {
2357 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2360 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2361 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2362
2363 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2364 amount: Amount::from_sat(1000),
2365 fee: Amount::ZERO,
2366 expiry_height: 1000,
2367 exit_delta: 128,
2368 user_keypair: alice_keypair.clone(),
2369 server_keypair: server_keypair.clone()
2370 }.build();
2371
2372 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2373
2374 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2376 alice_vtxo,
2377 vec![
2378 ArkoorDestination {
2379 total_amount: Amount::from_sat(600),
2380 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2381 },
2382 ArkoorDestination {
2383 total_amount: Amount::from_sat(200),
2384 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2385 },
2386 ArkoorDestination {
2387 total_amount: Amount::from_sat(200),
2388 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2389 }
2390 ],
2391 ).unwrap();
2392
2393 assert!(builder.unsigned_isolation_fanout_tx.is_some());
2395
2396 assert_eq!(builder.outputs.len(), 1);
2398 assert_eq!(builder.isolated_outputs.len(), 2);
2399 }
2400
2401 #[test]
2402 fn isolate_dust_split_successful() {
2403 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2407 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2408 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2409
2410 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2411 amount: Amount::from_sat(1000),
2412 fee: Amount::ZERO,
2413 expiry_height: 1000,
2414 exit_delta: 128,
2415 user_keypair: alice_keypair.clone(),
2416 server_keypair: server_keypair.clone()
2417 }.build();
2418
2419 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2420
2421 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2422 alice_vtxo,
2423 vec![
2424 ArkoorDestination {
2425 total_amount: Amount::from_sat(800),
2426 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2427 },
2428 ArkoorDestination {
2429 total_amount: Amount::from_sat(100),
2430 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2431 },
2432 ArkoorDestination {
2433 total_amount: Amount::from_sat(100),
2434 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2435 }
2436 ],
2437 ).unwrap();
2438
2439 assert!(builder.unsigned_isolation_fanout_tx.is_some());
2441
2442 assert_eq!(builder.outputs.len(), 1);
2444 assert_eq!(builder.isolated_outputs.len(), 3);
2445
2446 assert_eq!(builder.outputs[0].total_amount, Amount::from_sat(670));
2448 let isolated_sum: Amount = builder.isolated_outputs.iter().map(|o| o.total_amount).sum();
2449 assert_eq!(isolated_sum, P2TR_DUST);
2450 }
2451
2452 #[test]
2453 fn isolate_dust_split_impossible() {
2454 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2459 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2460 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2461
2462 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2463 amount: Amount::from_sat(600),
2464 fee: Amount::ZERO,
2465 expiry_height: 1000,
2466 exit_delta: 128,
2467 user_keypair: alice_keypair.clone(),
2468 server_keypair: server_keypair.clone()
2469 }.build();
2470
2471 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2472
2473 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2474 alice_vtxo,
2475 vec![
2476 ArkoorDestination {
2477 total_amount: Amount::from_sat(400),
2478 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2479 },
2480 ArkoorDestination {
2481 total_amount: Amount::from_sat(100),
2482 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2483 },
2484 ArkoorDestination {
2485 total_amount: Amount::from_sat(100),
2486 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2487 }
2488 ],
2489 ).unwrap();
2490
2491 assert!(builder.unsigned_isolation_fanout_tx.is_none());
2493
2494 assert_eq!(builder.outputs.len(), 3);
2496 assert_eq!(builder.isolated_outputs.len(), 0);
2497 }
2498
2499 #[test]
2500 fn isolate_dust_exactly_boundary() {
2501 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2505 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2506 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2507
2508 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2509 amount: Amount::from_sat(1000),
2510 fee: Amount::ZERO,
2511 expiry_height: 1000,
2512 exit_delta: 128,
2513 user_keypair: alice_keypair.clone(),
2514 server_keypair: server_keypair.clone()
2515 }.build();
2516
2517 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2518
2519 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2520 alice_vtxo,
2521 vec![
2522 ArkoorDestination {
2523 total_amount: Amount::from_sat(660),
2524 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2525 },
2526 ArkoorDestination {
2527 total_amount: Amount::from_sat(170),
2528 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2529 },
2530 ArkoorDestination {
2531 total_amount: Amount::from_sat(170),
2532 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2533 }
2534 ],
2535 ).unwrap();
2536
2537 assert!(builder.unsigned_isolation_fanout_tx.is_some());
2539
2540 assert_eq!(builder.outputs.len(), 1);
2542 assert_eq!(builder.isolated_outputs.len(), 2);
2543
2544 assert_eq!(builder.outputs[0].total_amount, Amount::from_sat(660));
2546 assert_eq!(builder.isolated_outputs[0].total_amount, Amount::from_sat(170));
2547 assert_eq!(builder.isolated_outputs[1].total_amount, Amount::from_sat(170));
2548 }
2549}