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 fn build_internal_vtxos(
700 &self,
701 intermediate_sig: Option<schnorr::Signature>,
702 ) -> Vec<(ServerVtxo<Full>, Txid)> {
703 let mut ret = Vec::new();
704
705 if self.checkpoint_data.is_some() {
706 for idx in 0..self.outputs.len() {
707 let vtxo = self.build_checkpoint_vtxo_at(idx, intermediate_sig);
708 let spending_txid = self.unsigned_arkoor_txs[idx].compute_txid();
709 ret.push((vtxo, spending_txid));
710 }
711 }
712
713 if !self.isolated_outputs.is_empty() {
714 let output_idx = self.outputs.len();
715
716 let (int_tx, int_txid) = if let Some((tx, txid)) = &self.checkpoint_data {
717 (tx, *txid)
718 } else {
719 let arkoor_tx = &self.unsigned_arkoor_txs[0];
720 (arkoor_tx, arkoor_tx.compute_txid())
721 };
722
723 let vtxo = Vtxo {
724 amount: self.isolated_outputs.iter().map(|o| o.total_amount).sum(),
725 policy: ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey()),
726 expiry_height: self.input.expiry_height,
727 server_pubkey: self.input.server_pubkey,
728 exit_delta: self.input.exit_delta,
729 point: OutPoint::new(int_txid, output_idx as u32),
730 anchor_point: self.input.anchor_point,
731 genesis: Full {
732 items: self.input.genesis.items.clone().into_iter().chain([
733 GenesisItem {
734 transition: GenesisTransition::new_arkoor(
735 vec![self.input.user_pubkey()],
736 self.input_tweak,
737 intermediate_sig,
738 ),
739 output_idx: output_idx as u8,
740 other_outputs: int_tx.output.iter().enumerate()
741 .filter_map(|(i, txout)| {
742 if i == output_idx || txout.is_p2a_fee_anchor() {
743 None
744 } else {
745 Some(txout.clone())
746 }
747 })
748 .collect(),
749 fee_amount: Amount::ZERO,
750 },
751 ]).collect(),
752 },
753 };
754
755 let spending_txid = self.unsigned_isolation_fanout_tx.as_ref()
756 .expect("isolation fanout tx must exist when isolated_outputs is non-empty")
757 .compute_txid();
758 ret.push((vtxo, spending_txid));
759 }
760
761 ret
762 }
763
764 pub fn input_spend_info(&self) -> (VtxoId, Txid) {
766 if let Some((_tx, checkpoint_txid)) = &self.checkpoint_data {
767 (self.input.id(), *checkpoint_txid)
768 } else {
769 (self.input.id(), self.unsigned_arkoor_txs[0].compute_txid())
770 }
771 }
772
773 pub fn build_unsigned_internal_vtxos(&self) -> Vec<(ServerVtxo<Full>, Txid)> {
776 self.build_internal_vtxos(None)
777 }
778
779 pub fn spend_info(&self) -> Vec<(VtxoId, Txid)> {
781 let mut ret = vec![self.input_spend_info()];
782 for (vtxo, spending_txid) in self.build_unsigned_internal_vtxos() {
783 ret.push((vtxo.id(), spending_txid));
784 }
785 ret
786 }
787
788 pub fn virtual_transactions(&self) -> Vec<Txid> {
793 let mut ret = Vec::new();
794 if let Some((_, txid)) = &self.checkpoint_data {
796 ret.push(*txid);
797 }
798 ret.extend(self.unsigned_arkoor_txs.iter().map(|tx| tx.compute_txid()));
800 if let Some(tx) = &self.unsigned_isolation_fanout_tx {
802 ret.push(tx.compute_txid());
803 }
804 ret
805 }
806
807 fn taptweak_at(&self, idx: usize) -> TapTweakHash {
808 if idx == 0 { self.input_tweak } else { self.checkpoint_policy_tweak }
809 }
810
811 fn user_pubkey(&self) -> PublicKey {
812 self.input.user_pubkey()
813 }
814
815 fn server_pubkey(&self) -> PublicKey {
816 self.input.server_pubkey()
817 }
818
819 fn construct_unsigned_checkpoint_tx<G>(
824 input: &Vtxo<G>,
825 outputs: &[ArkoorDestination],
826 dust_isolation_amount: Option<Amount>,
827 ) -> Transaction {
828
829 let output_policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
831 let checkpoint_spk = output_policy
832 .script_pubkey(input.server_pubkey(), input.exit_delta(), input.expiry_height());
833
834 Transaction {
835 version: bitcoin::transaction::Version(3),
836 lock_time: bitcoin::absolute::LockTime::ZERO,
837 input: vec![TxIn {
838 previous_output: input.point(),
839 script_sig: ScriptBuf::new(),
840 sequence: Sequence::ZERO,
841 witness: Witness::new(),
842 }],
843 output: outputs.iter().map(|o| {
844 TxOut {
845 value: o.total_amount,
846 script_pubkey: checkpoint_spk.clone(),
847 }
848 })
849 .chain(dust_isolation_amount.map(|amt| {
851 TxOut {
852 value: amt,
853 script_pubkey: checkpoint_spk.clone(),
854 }
855 }))
856 .chain([fee::fee_anchor()]).collect()
857 }
858 }
859
860 fn construct_unsigned_arkoor_txs<G>(
861 input: &Vtxo<G>,
862 outputs: &[ArkoorDestination],
863 checkpoint_txid: Option<Txid>,
864 dust_isolation_amount: Option<Amount>,
865 ) -> Vec<Transaction> {
866
867 if let Some(checkpoint_txid) = checkpoint_txid {
868 let mut arkoor_txs = Vec::with_capacity(outputs.len());
870
871 for (vout, output) in outputs.iter().enumerate() {
872 let transaction = Transaction {
873 version: bitcoin::transaction::Version(3),
874 lock_time: bitcoin::absolute::LockTime::ZERO,
875 input: vec![TxIn {
876 previous_output: OutPoint::new(checkpoint_txid, vout as u32),
877 script_sig: ScriptBuf::new(),
878 sequence: Sequence::ZERO,
879 witness: Witness::new(),
880 }],
881 output: vec![
882 output.policy.txout(
883 output.total_amount,
884 input.server_pubkey(),
885 input.exit_delta(),
886 input.expiry_height(),
887 ),
888 fee::fee_anchor(),
889 ]
890 };
891 arkoor_txs.push(transaction);
892 }
893
894 arkoor_txs
895 } else {
896 let checkpoint_policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
898 let checkpoint_spk = checkpoint_policy.script_pubkey(
899 input.server_pubkey(),
900 input.exit_delta(),
901 input.expiry_height()
902 );
903
904 let transaction = Transaction {
905 version: bitcoin::transaction::Version(3),
906 lock_time: bitcoin::absolute::LockTime::ZERO,
907 input: vec![TxIn {
908 previous_output: input.point(),
909 script_sig: ScriptBuf::new(),
910 sequence: Sequence::ZERO,
911 witness: Witness::new(),
912 }],
913 output: outputs.iter()
914 .map(|o| o.policy.txout(
915 o.total_amount,
916 input.server_pubkey(),
917 input.exit_delta(),
918 input.expiry_height(),
919 ))
920 .chain(dust_isolation_amount.map(|amt| TxOut {
922 value: amt,
923 script_pubkey: checkpoint_spk.clone(),
924 }))
925 .chain([fee::fee_anchor()])
926 .collect()
927 };
928 vec![transaction]
929 }
930 }
931
932 fn construct_unsigned_isolation_fanout_tx<G>(
940 input: &Vtxo<G>,
941 isolated_outputs: &[ArkoorDestination],
942 parent_txid: Txid, dust_isolation_output_vout: u32, ) -> Transaction {
945
946 Transaction {
947 version: bitcoin::transaction::Version(3),
948 lock_time: bitcoin::absolute::LockTime::ZERO,
949 input: vec![TxIn {
950 previous_output: OutPoint::new(parent_txid, dust_isolation_output_vout),
951 script_sig: ScriptBuf::new(),
952 sequence: Sequence::ZERO,
953 witness: Witness::new(),
954 }],
955 output: isolated_outputs.iter().map(|o| {
956 TxOut {
957 value: o.total_amount,
958 script_pubkey: o.policy.script_pubkey(
959 input.server_pubkey(),
960 input.exit_delta(),
961 input.expiry_height(),
962 ),
963 }
964 }).chain([fee::fee_anchor()]).collect(),
965 }
966 }
967
968 fn validate_amounts<G>(
969 input: &Vtxo<G>,
970 outputs: &[ArkoorDestination],
971 isolation_outputs: &[ArkoorDestination],
972 ) -> Result<(), ArkoorConstructionError> {
973
974 let input_amount = input.amount();
979 let output_amount = outputs.iter().chain(isolation_outputs.iter())
980 .map(|o| o.total_amount).sum::<Amount>();
981
982 if input_amount != output_amount {
983 return Err(ArkoorConstructionError::Unbalanced {
984 input: input_amount,
985 output: output_amount,
986 })
987 }
988
989 if outputs.is_empty() {
991 return Err(ArkoorConstructionError::NoOutputs)
992 }
993
994 if !isolation_outputs.is_empty() {
996 let isolation_sum: Amount = isolation_outputs.iter()
997 .map(|o| o.total_amount).sum();
998 if isolation_sum < P2TR_DUST {
999 return Err(ArkoorConstructionError::Dust)
1000 }
1001 }
1002
1003 Ok(())
1004 }
1005
1006
1007 fn to_state<S2: state::BuilderState>(self) -> ArkoorBuilder<S2> {
1008 ArkoorBuilder {
1009 input: self.input,
1010 outputs: self.outputs,
1011 isolated_outputs: self.isolated_outputs,
1012 checkpoint_data: self.checkpoint_data,
1013 unsigned_arkoor_txs: self.unsigned_arkoor_txs,
1014 unsigned_isolation_fanout_tx: self.unsigned_isolation_fanout_tx,
1015 new_vtxo_ids: self.new_vtxo_ids,
1016 sighashes: self.sighashes,
1017 input_tweak: self.input_tweak,
1018 checkpoint_policy_tweak: self.checkpoint_policy_tweak,
1019 user_keypair: self.user_keypair,
1020 user_pub_nonces: self.user_pub_nonces,
1021 user_sec_nonces: self.user_sec_nonces,
1022 server_pub_nonces: self.server_pub_nonces,
1023 server_partial_sigs: self.server_partial_sigs,
1024 full_signatures: self.full_signatures,
1025 _state: PhantomData,
1026 }
1027 }
1028}
1029
1030impl ArkoorBuilder<state::Initial> {
1031 pub fn new_with_checkpoint(
1033 input: Vtxo<Full>,
1034 outputs: Vec<ArkoorDestination>,
1035 isolated_outputs: Vec<ArkoorDestination>,
1036 ) -> Result<Self, ArkoorConstructionError> {
1037 Self::new(input, outputs, isolated_outputs, true)
1038 }
1039
1040 pub fn new_without_checkpoint(
1042 input: Vtxo<Full>,
1043 outputs: Vec<ArkoorDestination>,
1044 isolated_outputs: Vec<ArkoorDestination>,
1045 ) -> Result<Self, ArkoorConstructionError> {
1046 Self::new(input, outputs, isolated_outputs, false)
1047 }
1048
1049 pub fn new_with_checkpoint_isolate_dust(
1054 input: Vtxo<Full>,
1055 outputs: Vec<ArkoorDestination>,
1056 ) -> Result<Self, ArkoorConstructionError> {
1057 Self::new_isolate_dust(input, outputs, true)
1058 }
1059
1060 pub(crate) fn new_isolate_dust(
1061 input: Vtxo<Full>,
1062 outputs: Vec<ArkoorDestination>,
1063 use_checkpoints: bool,
1064 ) -> Result<Self, ArkoorConstructionError> {
1065 if outputs.iter().all(|v| v.total_amount >= P2TR_DUST)
1067 || outputs.iter().all(|v| v.total_amount < P2TR_DUST)
1068 {
1069 return Self::new(input, outputs, vec![], use_checkpoints);
1070 }
1071
1072 let (mut dust, mut non_dust) = outputs.iter().cloned()
1074 .partition::<Vec<_>, _>(|v| v.total_amount < P2TR_DUST);
1075
1076 let dust_sum = dust.iter().map(|o| o.total_amount).sum::<Amount>();
1077 if dust_sum >= P2TR_DUST {
1078 return Self::new(input, non_dust, dust, use_checkpoints);
1079 }
1080
1081 let non_dust_sum = non_dust.iter().map(|o| o.total_amount).sum::<Amount>();
1083 if non_dust_sum < P2TR_DUST * 2 {
1084 return Self::new(input, outputs, vec![], use_checkpoints);
1085 }
1086
1087 let deficit = P2TR_DUST - dust_sum;
1089 let split_idx = non_dust.iter()
1092 .position(|o| o.total_amount - deficit >= P2TR_DUST);
1093
1094 if let Some(idx) = split_idx {
1095 let output_to_split = non_dust[idx].clone();
1096
1097 let dust_piece = ArkoorDestination {
1098 total_amount: deficit,
1099 policy: output_to_split.policy.clone(),
1100 };
1101 let leftover = ArkoorDestination {
1102 total_amount: output_to_split.total_amount - deficit,
1103 policy: output_to_split.policy,
1104 };
1105
1106 non_dust[idx] = leftover;
1107 dust.insert(0, dust_piece);
1109
1110 return Self::new(input, non_dust, dust, use_checkpoints);
1111 } else {
1112 let all_outputs = non_dust.into_iter().chain(dust).collect();
1114 return Self::new(input, all_outputs, vec![], use_checkpoints);
1115 }
1116 }
1117
1118 pub(crate) fn new(
1119 input: Vtxo<Full>,
1120 outputs: Vec<ArkoorDestination>,
1121 isolated_outputs: Vec<ArkoorDestination>,
1122 use_checkpoint: bool,
1123 ) -> Result<Self, ArkoorConstructionError> {
1124 Self::validate_amounts(&input, &outputs, &isolated_outputs)?;
1126
1127 let combined_dust_amount = if !isolated_outputs.is_empty() {
1129 Some(isolated_outputs.iter().map(|o| o.total_amount).sum())
1130 } else {
1131 None
1132 };
1133
1134 let unsigned_checkpoint_tx = if use_checkpoint {
1136 let tx = Self::construct_unsigned_checkpoint_tx(
1137 &input,
1138 &outputs,
1139 combined_dust_amount,
1140 );
1141 let txid = tx.compute_txid();
1142 Some((tx, txid))
1143 } else {
1144 None
1145 };
1146
1147 let unsigned_arkoor_txs = Self::construct_unsigned_arkoor_txs(
1149 &input,
1150 &outputs,
1151 unsigned_checkpoint_tx.as_ref().map(|t| t.1),
1152 combined_dust_amount,
1153 );
1154
1155 let unsigned_isolation_fanout_tx = if !isolated_outputs.is_empty() {
1157 let dust_isolation_output_vout = outputs.len() as u32;
1160
1161 let parent_txid = if let Some((_tx, txid)) = &unsigned_checkpoint_tx {
1162 *txid
1163 } else {
1164 unsigned_arkoor_txs[0].compute_txid()
1165 };
1166
1167 Some(Self::construct_unsigned_isolation_fanout_tx(
1168 &input,
1169 &isolated_outputs,
1170 parent_txid,
1171 dust_isolation_output_vout,
1172 ))
1173 } else {
1174 None
1175 };
1176
1177 let new_vtxo_ids = unsigned_arkoor_txs.iter()
1179 .map(|tx| OutPoint::new(tx.compute_txid(), 0))
1180 .map(|outpoint| VtxoId::from(outpoint))
1181 .collect();
1182
1183 let mut sighashes = Vec::new();
1185
1186 if let Some((checkpoint_tx, _txid)) = &unsigned_checkpoint_tx {
1187 sighashes.push(arkoor_sighash(&input.txout(), checkpoint_tx));
1189
1190 for vout in 0..outputs.len() {
1192 let prevout = checkpoint_tx.output[vout].clone();
1193 sighashes.push(arkoor_sighash(&prevout, &unsigned_arkoor_txs[vout]));
1194 }
1195 } else {
1196 sighashes.push(arkoor_sighash(&input.txout(), &unsigned_arkoor_txs[0]));
1198 }
1199
1200 if let Some(ref tx) = unsigned_isolation_fanout_tx {
1202 let dust_output_vout = outputs.len(); let prevout = if let Some((checkpoint_tx, _txid)) = &unsigned_checkpoint_tx {
1204 checkpoint_tx.output[dust_output_vout].clone()
1205 } else {
1206 unsigned_arkoor_txs[0].output[dust_output_vout].clone()
1208 };
1209 sighashes.push(arkoor_sighash(&prevout, tx));
1210 }
1211
1212 let policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
1214 let input_tweak = input.output_taproot().tap_tweak();
1215 let checkpoint_policy_tweak = policy.taproot(
1216 input.server_pubkey(),
1217 input.exit_delta(),
1218 input.expiry_height(),
1219 ).tap_tweak();
1220
1221 Ok(Self {
1222 input: input,
1223 outputs: outputs,
1224 isolated_outputs,
1225 sighashes: sighashes,
1226 input_tweak,
1227 checkpoint_policy_tweak,
1228 checkpoint_data: unsigned_checkpoint_tx,
1229 unsigned_arkoor_txs: unsigned_arkoor_txs,
1230 unsigned_isolation_fanout_tx,
1231 new_vtxo_ids: new_vtxo_ids,
1232 user_keypair: None,
1233 user_pub_nonces: None,
1234 user_sec_nonces: None,
1235 server_pub_nonces: None,
1236 server_partial_sigs: None,
1237 full_signatures: None,
1238 _state: PhantomData,
1239 })
1240 }
1241
1242 pub fn generate_user_nonces(
1245 mut self,
1246 user_keypair: Keypair,
1247 ) -> ArkoorBuilder<state::UserGeneratedNonces> {
1248 let mut user_pub_nonces = Vec::with_capacity(self.nb_sigs());
1249 let mut user_sec_nonces = Vec::with_capacity(self.nb_sigs());
1250
1251 for idx in 0..self.nb_sigs() {
1252 let sighash = &self.sighashes[idx].to_byte_array();
1253 let (sec_nonce, pub_nonce) = musig::nonce_pair_with_msg(&user_keypair, sighash);
1254
1255 user_pub_nonces.push(pub_nonce);
1256 user_sec_nonces.push(sec_nonce);
1257 }
1258
1259 self.user_keypair = Some(user_keypair);
1260 self.user_pub_nonces = Some(user_pub_nonces);
1261 self.user_sec_nonces = Some(user_sec_nonces);
1262
1263 self.to_state::<state::UserGeneratedNonces>()
1264 }
1265
1266 fn set_user_pub_nonces(
1273 mut self,
1274 user_pub_nonces: Vec<musig::PublicNonce>,
1275 ) -> Result<ArkoorBuilder<state::ServerCanCosign>, ArkoorSigningError> {
1276 if user_pub_nonces.len() != self.nb_sigs() {
1277 return Err(ArkoorSigningError::InvalidNbUserNonces {
1278 expected: self.nb_sigs(),
1279 got: user_pub_nonces.len()
1280 })
1281 }
1282
1283 self.user_pub_nonces = Some(user_pub_nonces);
1284 Ok(self.to_state::<state::ServerCanCosign>())
1285 }
1286
1287 pub fn cosign_both(
1292 mut self,
1293 user_keypair: &Keypair,
1294 server_keypair: &Keypair,
1295 ) -> Result<ArkoorBuilder<state::UserSigned>, ArkoorSigningError> {
1296 if user_keypair.public_key() != self.input.user_pubkey() {
1297 return Err(ArkoorSigningError::IncorrectKey {
1298 expected: self.input.user_pubkey(),
1299 got: user_keypair.public_key(),
1300 });
1301 }
1302 if server_keypair.public_key() != self.input.server_pubkey() {
1303 return Err(ArkoorSigningError::IncorrectKey {
1304 expected: self.input.server_pubkey(),
1305 got: server_keypair.public_key(),
1306 });
1307 }
1308
1309 let mut sigs = Vec::with_capacity(self.nb_sigs());
1310 for idx in 0..self.nb_sigs() {
1311 sigs.push(musig::cosign_both(
1312 user_keypair,
1313 server_keypair,
1314 self.sighashes[idx].to_byte_array(),
1315 Some(self.taptweak_at(idx).to_byte_array()),
1316 ));
1317 }
1318
1319 self.full_signatures = Some(sigs);
1320 Ok(self.to_state::<state::UserSigned>())
1321 }
1322}
1323
1324impl<'a> ArkoorBuilder<state::ServerCanCosign> {
1325 pub fn from_cosign_request(
1326 cosign_request: ArkoorCosignRequest<Vtxo<Full>>,
1327 ) -> Result<ArkoorBuilder<state::ServerCanCosign>, ArkoorSigningError> {
1328 cosign_request.verify_attestation()
1329 .map_err(ArkoorSigningError::InvalidAttestation)?;
1330
1331 let ret = ArkoorBuilder::new(
1332 cosign_request.input,
1333 cosign_request.outputs,
1334 cosign_request.isolated_outputs,
1335 cosign_request.use_checkpoint,
1336 )
1337 .map_err(ArkoorSigningError::ArkoorConstructionError)?
1338 .set_user_pub_nonces(cosign_request.user_pub_nonces.clone())?;
1339 Ok(ret)
1340 }
1341
1342 pub fn server_cosign(
1343 mut self,
1344 server_keypair: &Keypair,
1345 ) -> Result<ArkoorBuilder<state::ServerSigned>, ArkoorSigningError> {
1346 if server_keypair.public_key() != self.input.server_pubkey() {
1348 return Err(ArkoorSigningError::IncorrectKey {
1349 expected: self.input.server_pubkey(),
1350 got: server_keypair.public_key(),
1351 });
1352 }
1353
1354 let mut server_pub_nonces = Vec::with_capacity(self.outputs.len() + 1);
1355 let mut server_partial_sigs = Vec::with_capacity(self.outputs.len() + 1);
1356
1357 for idx in 0..self.nb_sigs() {
1358 let (server_pub_nonce, server_partial_sig) = musig::deterministic_partial_sign(
1359 &server_keypair,
1360 [self.input.user_pubkey()],
1361 &[&self.user_pub_nonces.as_ref().expect("state-invariant")[idx]],
1362 self.sighashes[idx].to_byte_array(),
1363 Some(self.taptweak_at(idx).to_byte_array()),
1364 );
1365
1366 server_pub_nonces.push(server_pub_nonce);
1367 server_partial_sigs.push(server_partial_sig);
1368 };
1369
1370 self.server_pub_nonces = Some(server_pub_nonces);
1371 self.server_partial_sigs = Some(server_partial_sigs);
1372 Ok(self.to_state::<state::ServerSigned>())
1373 }
1374}
1375
1376impl ArkoorBuilder<state::ServerSigned> {
1377 pub fn user_pub_nonces(&self) -> Vec<musig::PublicNonce> {
1378 self.user_pub_nonces.as_ref().expect("state invariant").clone()
1379 }
1380
1381 pub fn server_partial_signatures(&self) -> Vec<musig::PartialSignature> {
1382 self.server_partial_sigs.as_ref().expect("state invariant").clone()
1383 }
1384
1385 pub fn cosign_response(&self) -> ArkoorCosignResponse {
1386 ArkoorCosignResponse {
1387 server_pub_nonces: self.server_pub_nonces.as_ref()
1388 .expect("state invariant").clone(),
1389 server_partial_sigs: self.server_partial_sigs.as_ref()
1390 .expect("state invariant").clone(),
1391 }
1392 }
1393}
1394
1395impl ArkoorBuilder<state::UserGeneratedNonces> {
1396 pub fn user_pub_nonces(&self) -> &[PublicNonce] {
1397 self.user_pub_nonces.as_ref().expect("State invariant")
1398 }
1399
1400 pub fn cosign_request(&self) -> ArkoorCosignRequest<Vtxo<Full>> {
1401 ArkoorCosignRequest::new(
1402 self.user_pub_nonces().to_vec(),
1403 self.input.clone(),
1404 self.outputs.clone(),
1405 self.isolated_outputs.clone(),
1406 self.checkpoint_data.is_some(),
1407 self.user_keypair.as_ref().expect("State invariant"),
1408 )
1409 }
1410
1411 fn validate_server_cosign_response(
1412 &self,
1413 data: &ArkoorCosignResponse,
1414 ) -> Result<(), ArkoorSigningError> {
1415
1416 if data.server_pub_nonces.len() != self.nb_sigs() {
1418 return Err(ArkoorSigningError::InvalidNbServerNonces {
1419 expected: self.nb_sigs(),
1420 got: data.server_pub_nonces.len(),
1421 });
1422 }
1423
1424 if data.server_partial_sigs.len() != self.nb_sigs() {
1425 return Err(ArkoorSigningError::InvalidNbServerPartialSigs {
1426 expected: self.nb_sigs(),
1427 got: data.server_partial_sigs.len(),
1428 })
1429 }
1430
1431 for idx in 0..self.nb_sigs() {
1433 let is_valid_sig = scripts::verify_partial_sig(
1434 self.sighashes[idx],
1435 self.taptweak_at(idx),
1436 (self.input.server_pubkey(), &data.server_pub_nonces[idx]),
1437 (self.input.user_pubkey(), &self.user_pub_nonces()[idx]),
1438 &data.server_partial_sigs[idx]
1439 );
1440
1441 if !is_valid_sig {
1442 return Err(ArkoorSigningError::InvalidPartialSignature {
1443 index: idx,
1444 });
1445 }
1446 }
1447 Ok(())
1448 }
1449
1450 pub fn user_cosign(
1451 mut self,
1452 user_keypair: &Keypair,
1453 server_cosign_data: &ArkoorCosignResponse,
1454 ) -> Result<ArkoorBuilder<state::UserSigned>, ArkoorSigningError> {
1455 if user_keypair.public_key() != self.input.user_pubkey() {
1457 return Err(ArkoorSigningError::IncorrectKey {
1458 expected: self.input.user_pubkey(),
1459 got: user_keypair.public_key(),
1460 });
1461 }
1462
1463 self.validate_server_cosign_response(&server_cosign_data)?;
1465
1466 let mut sigs = Vec::with_capacity(self.nb_sigs());
1467
1468 let user_sec_nonces = self.user_sec_nonces.take().expect("state invariant");
1471
1472 for (idx, user_sec_nonce) in user_sec_nonces.into_iter().enumerate() {
1473 let user_pub_nonce = self.user_pub_nonces()[idx];
1474 let server_pub_nonce = server_cosign_data.server_pub_nonces[idx];
1475 let agg_nonce = musig::nonce_agg(&[&user_pub_nonce, &server_pub_nonce]);
1476
1477 let (_partial, maybe_sig) = musig::partial_sign(
1478 [self.user_pubkey(), self.server_pubkey()],
1479 agg_nonce,
1480 &user_keypair,
1481 user_sec_nonce,
1482 self.sighashes[idx].to_byte_array(),
1483 Some(self.taptweak_at(idx).to_byte_array()),
1484 Some(&[&server_cosign_data.server_partial_sigs[idx]])
1485 );
1486
1487 let sig = maybe_sig.expect("The full signature exists. The server did sign first");
1488 sigs.push(sig);
1489 }
1490
1491 self.full_signatures = Some(sigs);
1492
1493 Ok(self.to_state::<state::UserSigned>())
1494 }
1495}
1496
1497
1498impl<'a> ArkoorBuilder<state::UserSigned> {
1499 pub fn build_signed_vtxos(&self) -> Vec<Vtxo<Full>> {
1500 let sigs = self.full_signatures.as_ref().expect("state invariant");
1501 let mut ret = Vec::with_capacity(self.outputs.len() + self.isolated_outputs.len());
1502
1503 if self.checkpoint_data.is_some() {
1504 let checkpoint_sig = sigs[0];
1505
1506 for i in 0..self.outputs.len() {
1508 let arkoor_sig = sigs[1 + i];
1509 ret.push(self.build_vtxo_at(i, Some(checkpoint_sig), Some(arkoor_sig)));
1510 }
1511
1512 if self.unsigned_isolation_fanout_tx.is_some() {
1514 let m = self.outputs.len();
1515 let fanout_tx_sig = sigs[1 + m];
1516
1517 for i in 0..self.isolated_outputs.len() {
1518 ret.push(self.build_isolated_vtxo_at(
1519 i,
1520 Some(checkpoint_sig),
1521 Some(fanout_tx_sig),
1522 ));
1523 }
1524 }
1525 } else {
1526 let arkoor_sig = sigs[0];
1528
1529 for i in 0..self.outputs.len() {
1531 ret.push(self.build_vtxo_at(i, None, Some(arkoor_sig)));
1532 }
1533
1534 if self.unsigned_isolation_fanout_tx.is_some() {
1536 let fanout_tx_sig = sigs[1];
1537
1538 for i in 0..self.isolated_outputs.len() {
1539 ret.push(self.build_isolated_vtxo_at(
1540 i,
1541 Some(arkoor_sig), Some(fanout_tx_sig),
1543 ));
1544 }
1545 }
1546 }
1547
1548 ret
1549 }
1550
1551 pub fn signed_virtual_transactions(&self) -> Vec<Transaction> {
1556 let sigs = self.full_signatures.as_ref().expect("state invariant");
1557 let mut ret = Vec::new();
1558 let mut sig_idx = 0;
1559 if let Some((tx, _)) = &self.checkpoint_data {
1560 let mut tx = tx.clone();
1561 tx.input[0].witness.push(&sigs[sig_idx][..]);
1562 ret.push(tx);
1563 sig_idx += 1;
1564 }
1565 for tx in &self.unsigned_arkoor_txs {
1566 let mut tx = tx.clone();
1567 tx.input[0].witness.push(&sigs[sig_idx][..]);
1568 ret.push(tx);
1569 sig_idx += 1;
1570 }
1571 if let Some(tx) = &self.unsigned_isolation_fanout_tx {
1572 let mut tx = tx.clone();
1573 tx.input[0].witness.push(&sigs[sig_idx][..]);
1574 ret.push(tx);
1575 }
1576 ret
1577 }
1578
1579 pub fn build_signed_internal_vtxos(&self) -> Vec<(ServerVtxo<Full>, Txid)> {
1582 let sigs = self.full_signatures.as_ref().expect("state invariant");
1583 let intermediate_sig = if self.checkpoint_data.is_some() || !self.isolated_outputs.is_empty() {
1584 Some(sigs[0])
1585 } else {
1586 None
1587 };
1588 self.build_internal_vtxos(intermediate_sig)
1589 }
1590}
1591
1592fn arkoor_sighash(prevout: &TxOut, arkoor_tx: &Transaction) -> TapSighash {
1593 let mut shc = SighashCache::new(arkoor_tx);
1594
1595 shc.taproot_key_spend_signature_hash(
1596 0, &sighash::Prevouts::All(&[prevout]), TapSighashType::Default,
1597 ).expect("sighash error")
1598}
1599
1600#[cfg(test)]
1601mod test {
1602 use super::*;
1603
1604 use std::collections::HashSet;
1605
1606 use bitcoin::Amount;
1607 use bitcoin::secp256k1::Keypair;
1608 use bitcoin::secp256k1::rand;
1609
1610 use crate::SECP;
1611 use crate::test_util::dummy::DummyTestVtxoSpec;
1612 use crate::vtxo::VtxoId;
1613
1614 fn verify_signed_internal_vtxos(
1616 builder: &ArkoorBuilder<state::UserSigned>,
1617 funding_tx: &Transaction,
1618 ) {
1619 let signed = builder.build_signed_internal_vtxos();
1620 let unsigned = builder.build_unsigned_internal_vtxos();
1621 assert_eq!(signed.len(), unsigned.len());
1622
1623 for (vtxo, _spending_txid) in &signed {
1624 vtxo.validate(funding_tx).expect("signed internal vtxo must be valid");
1625 }
1626 }
1627
1628 fn verify_builder<S: state::BuilderState>(
1630 builder: &ArkoorBuilder<S>,
1631 input: &Vtxo<Full>,
1632 outputs: &[ArkoorDestination],
1633 isolated_outputs: &[ArkoorDestination],
1634 ) {
1635 let has_isolation = !isolated_outputs.is_empty();
1636
1637 let spend_info = builder.spend_info();
1638 let spend_vtxo_ids: HashSet<VtxoId> = spend_info.iter().map(|(id, _)| *id).collect();
1639
1640 assert_eq!(spend_info[0].0, input.id());
1642
1643 assert_eq!(spend_vtxo_ids.len(), spend_info.len());
1645
1646 let internal_vtxos = builder.build_unsigned_internal_vtxos();
1648 let internal_vtxo_ids = internal_vtxos.iter().map(|(v, _)| v.id()).collect::<HashSet<_>>();
1649 for (internal_vtxo, _spending_txid) in &internal_vtxos {
1650 assert!(spend_vtxo_ids.contains(&internal_vtxo.id()));
1651 assert!(matches!(internal_vtxo.policy(), ServerVtxoPolicy::Checkpoint(_)));
1652 }
1653
1654 for (vtxo_id, _) in &spend_info[1..] {
1656 assert!(internal_vtxo_ids.contains(vtxo_id));
1657 }
1658
1659 if has_isolation {
1661 let (isolation_vtxo, _) = internal_vtxos.last().unwrap();
1662 let expected_isolation_amount: Amount = isolated_outputs.iter()
1663 .map(|o| o.total_amount)
1664 .sum();
1665 assert_eq!(isolation_vtxo.amount(), expected_isolation_amount);
1666 }
1667
1668 let final_vtxos = builder.build_unsigned_vtxos().collect::<Vec<_>>();
1670 for final_vtxo in &final_vtxos {
1671 assert!(!spend_vtxo_ids.contains(&final_vtxo.id()));
1672 }
1673
1674 let all_destinations = outputs.iter()
1676 .chain(isolated_outputs.iter())
1677 .collect::<Vec<&_>>();
1678 for (vtxo, dest) in final_vtxos.iter().zip(all_destinations.iter()) {
1679 assert_eq!(vtxo.amount(), dest.total_amount);
1680 assert_eq!(vtxo.policy, dest.policy);
1681 }
1682
1683 let total_output_amount: Amount = final_vtxos.iter().map(|v| v.amount()).sum();
1685 assert_eq!(total_output_amount, input.amount());
1686 }
1687
1688 #[test]
1689 fn build_checkpointed_arkoor() {
1690 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1691 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1692 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1693
1694 println!("Alice keypair: {}", alice_keypair.public_key());
1695 println!("Bob keypair: {}", bob_keypair.public_key());
1696 println!("Server keypair: {}", server_keypair.public_key());
1697 println!("-----------------------------------------------");
1698
1699 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1700 amount: Amount::from_sat(100_330),
1701 fee: Amount::from_sat(330),
1702 expiry_height: 1000,
1703 exit_delta : 128,
1704 user_keypair: alice_keypair.clone(),
1705 server_keypair: server_keypair.clone()
1706 }.build();
1707
1708 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1710
1711 let dest = vec![
1712 ArkoorDestination {
1713 total_amount: Amount::from_sat(96_000),
1714 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1715 },
1716 ArkoorDestination {
1717 total_amount: Amount::from_sat(4_000),
1718 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1719 }
1720 ];
1721
1722 let user_builder = ArkoorBuilder::new_with_checkpoint(
1723 alice_vtxo.clone(),
1724 dest.clone(),
1725 vec![], ).expect("Valid arkoor request");
1727
1728 verify_builder(&user_builder, &alice_vtxo, &dest, &[]);
1729
1730 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1731 let cosign_request = user_builder.cosign_request();
1732
1733 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1735 .expect("Invalid cosign request")
1736 .server_cosign(&server_keypair)
1737 .expect("Incorrect key");
1738
1739 let cosign_data = server_builder.cosign_response();
1740
1741 let signed_builder = user_builder
1743 .user_cosign(&alice_keypair, &cosign_data)
1744 .expect("Valid cosign data and correct key");
1745 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
1746 let vtxos = signed_builder.build_signed_vtxos();
1747
1748 for vtxo in vtxos.into_iter() {
1749 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1751
1752 let mut prev_tx = funding_tx.clone();
1754 for tx in vtxo.transactions().map(|item| item.tx) {
1755 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1756 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1757 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1758 prev_tx = tx;
1759 }
1760 }
1761
1762 }
1763
1764 #[test]
1765 fn build_checkpointed_arkoor_with_dust_isolation() {
1766 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1769 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1770 let charlie_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1771 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1772
1773 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1774 amount: Amount::from_sat(100_330),
1775 fee: Amount::from_sat(330),
1776 expiry_height: 1000,
1777 exit_delta : 128,
1778 user_keypair: alice_keypair.clone(),
1779 server_keypair: server_keypair.clone()
1780 }.build();
1781
1782 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1784
1785 let outputs = vec![
1787 ArkoorDestination {
1788 total_amount: Amount::from_sat(99_600),
1789 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1790 },
1791 ];
1792
1793 let dust_outputs = vec![
1795 ArkoorDestination {
1796 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(charlie_keypair.public_key())
1798 },
1799 ArkoorDestination {
1800 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1802 }
1803 ];
1804
1805 let user_builder = ArkoorBuilder::new_with_checkpoint(
1806 alice_vtxo.clone(),
1807 outputs.clone(),
1808 dust_outputs.clone(),
1809 ).expect("Valid arkoor request with dust isolation");
1810
1811 verify_builder(&user_builder, &alice_vtxo, &outputs, &dust_outputs);
1812
1813 assert!(
1815 user_builder.unsigned_isolation_fanout_tx.is_some(),
1816 "Dust isolation should be active",
1817 );
1818
1819 assert_eq!(user_builder.nb_sigs(), 3);
1821
1822 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1823 let cosign_request = user_builder.cosign_request();
1824
1825 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1827 .expect("Invalid cosign request")
1828 .server_cosign(&server_keypair)
1829 .expect("Incorrect key");
1830
1831 let cosign_data = server_builder.cosign_response();
1832
1833 let signed_builder = user_builder
1835 .user_cosign(&alice_keypair, &cosign_data)
1836 .expect("Valid cosign data and correct key");
1837 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
1838 let vtxos = signed_builder.build_signed_vtxos();
1839
1840 assert_eq!(vtxos.len(), 3);
1842
1843 for vtxo in vtxos.into_iter() {
1844 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1846
1847 let mut prev_tx = funding_tx.clone();
1849 for tx in vtxo.transactions().map(|item| item.tx) {
1850 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1851 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1852 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1853 prev_tx = tx;
1854 }
1855 }
1856 }
1857
1858 #[test]
1859 fn build_no_checkpoint_arkoor() {
1860 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1861 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1862 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1863
1864 println!("Alice keypair: {}", alice_keypair.public_key());
1865 println!("Bob keypair: {}", bob_keypair.public_key());
1866 println!("Server keypair: {}", server_keypair.public_key());
1867 println!("-----------------------------------------------");
1868
1869 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1870 amount: Amount::from_sat(100_330),
1871 fee: Amount::from_sat(330),
1872 expiry_height: 1000,
1873 exit_delta : 128,
1874 user_keypair: alice_keypair.clone(),
1875 server_keypair: server_keypair.clone()
1876 }.build();
1877
1878 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1880
1881 let dest = vec![
1882 ArkoorDestination {
1883 total_amount: Amount::from_sat(96_000),
1884 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1885 },
1886 ArkoorDestination {
1887 total_amount: Amount::from_sat(4_000),
1888 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1889 }
1890 ];
1891
1892 let user_builder = ArkoorBuilder::new_without_checkpoint(
1893 alice_vtxo.clone(),
1894 dest.clone(),
1895 vec![], ).expect("Valid arkoor request");
1897
1898 verify_builder(&user_builder, &alice_vtxo, &dest, &[]);
1899
1900 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1901 let cosign_request = user_builder.cosign_request();
1902
1903 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1905 .expect("Invalid cosign request")
1906 .server_cosign(&server_keypair)
1907 .expect("Incorrect key");
1908
1909 let cosign_data = server_builder.cosign_response();
1910
1911 let signed_builder = user_builder
1913 .user_cosign(&alice_keypair, &cosign_data)
1914 .expect("Valid cosign data and correct key");
1915 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
1916 let vtxos = signed_builder.build_signed_vtxos();
1917
1918 for vtxo in vtxos.into_iter() {
1919 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1921
1922 let mut prev_tx = funding_tx.clone();
1924 for tx in vtxo.transactions().map(|item| item.tx) {
1925 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1926 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1927 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1928 prev_tx = tx;
1929 }
1930 }
1931
1932 }
1933
1934 #[test]
1935 fn build_no_checkpoint_arkoor_with_dust_isolation() {
1936 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1939 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1940 let charlie_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1941 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1942
1943 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1944 amount: Amount::from_sat(100_330),
1945 fee: Amount::from_sat(330),
1946 expiry_height: 1000,
1947 exit_delta : 128,
1948 user_keypair: alice_keypair.clone(),
1949 server_keypair: server_keypair.clone()
1950 }.build();
1951
1952 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1954
1955 let outputs = vec![
1957 ArkoorDestination {
1958 total_amount: Amount::from_sat(99_600),
1959 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1960 },
1961 ];
1962
1963 let dust_outputs = vec![
1965 ArkoorDestination {
1966 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(charlie_keypair.public_key())
1968 },
1969 ArkoorDestination {
1970 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1972 }
1973 ];
1974
1975 let user_builder = ArkoorBuilder::new_without_checkpoint(
1976 alice_vtxo.clone(),
1977 outputs.clone(),
1978 dust_outputs.clone(),
1979 ).expect("Valid arkoor request with dust isolation");
1980
1981 verify_builder(&user_builder, &alice_vtxo, &outputs, &dust_outputs);
1982
1983 assert!(
1985 user_builder.unsigned_isolation_fanout_tx.is_some(),
1986 "Dust isolation should be active",
1987 );
1988
1989 assert_eq!(user_builder.nb_sigs(), 2);
1992
1993 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1994 let cosign_request = user_builder.cosign_request();
1995
1996 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1998 .expect("Invalid cosign request")
1999 .server_cosign(&server_keypair)
2000 .expect("Incorrect key");
2001
2002 let cosign_data = server_builder.cosign_response();
2003
2004 let signed_builder = user_builder
2006 .user_cosign(&alice_keypair, &cosign_data)
2007 .expect("Valid cosign data and correct key");
2008 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
2009 let vtxos = signed_builder.build_signed_vtxos();
2010
2011 assert_eq!(vtxos.len(), 3);
2013
2014 for vtxo in vtxos.into_iter() {
2015 vtxo.validate(&funding_tx).expect("Invalid VTXO");
2017
2018 let mut prev_tx = funding_tx.clone();
2020 for tx in vtxo.transactions().map(|item| item.tx) {
2021 let prev_outpoint: OutPoint = tx.input[0].previous_output;
2022 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
2023 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
2024 prev_tx = tx;
2025 }
2026 }
2027 }
2028
2029 #[test]
2030 fn build_checkpointed_arkoor_outputs_must_be_above_dust_if_mixed() {
2031 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2033 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2034 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2035
2036 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2037 amount: Amount::from_sat(1_330),
2038 fee: Amount::from_sat(330),
2039 expiry_height: 1000,
2040 exit_delta : 128,
2041 user_keypair: alice_keypair.clone(),
2042 server_keypair: server_keypair.clone()
2043 }.build();
2044
2045 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
2046
2047 ArkoorBuilder::new_with_checkpoint(
2049 alice_vtxo.clone(),
2050 vec![
2051 ArkoorDestination {
2052 total_amount: Amount::from_sat(100), policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2054 }; 10
2055 ],
2056 vec![],
2057 ).unwrap();
2058
2059 let res_empty = ArkoorBuilder::new_with_checkpoint(
2061 alice_vtxo.clone(),
2062 vec![],
2063 vec![
2064 ArkoorDestination {
2065 total_amount: Amount::from_sat(100), policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2067 }; 10
2068 ],
2069 );
2070 match res_empty {
2071 Err(ArkoorConstructionError::NoOutputs) => {},
2072 _ => panic!("Expected NoOutputs error for empty outputs"),
2073 }
2074
2075 ArkoorBuilder::new_with_checkpoint(
2077 alice_vtxo.clone(),
2078 vec![
2079 ArkoorDestination {
2080 total_amount: Amount::from_sat(330), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2082 }; 2
2083 ],
2084 vec![
2085 ArkoorDestination {
2086 total_amount: Amount::from_sat(170),
2087 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2088 }; 2
2089 ],
2090 ).unwrap();
2091
2092 let res_mixed_small = ArkoorBuilder::new_with_checkpoint(
2094 alice_vtxo.clone(),
2095 vec![
2096 ArkoorDestination {
2097 total_amount: Amount::from_sat(500),
2098 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2099 },
2100 ArkoorDestination {
2101 total_amount: Amount::from_sat(300),
2102 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2103 }
2104 ],
2105 vec![
2106 ArkoorDestination {
2107 total_amount: Amount::from_sat(100),
2108 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2109 }; 2 ],
2111 );
2112 match res_mixed_small {
2113 Err(ArkoorConstructionError::Dust) => {},
2114 _ => panic!("Expected Dust error for isolation sum < 330"),
2115 }
2116 }
2117
2118 #[test]
2119 fn build_checkpointed_arkoor_dust_sum_too_small() {
2120 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2122 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2123 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2124
2125 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2126 amount: Amount::from_sat(100_330),
2127 fee: Amount::from_sat(330),
2128 expiry_height: 1000,
2129 exit_delta : 128,
2130 user_keypair: alice_keypair.clone(),
2131 server_keypair: server_keypair.clone()
2132 }.build();
2133
2134 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
2135
2136 let outputs = vec![
2138 ArkoorDestination {
2139 total_amount: Amount::from_sat(99_900),
2140 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2141 },
2142 ];
2143
2144 let dust_outputs = vec![
2146 ArkoorDestination {
2147 total_amount: Amount::from_sat(50),
2148 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2149 },
2150 ArkoorDestination {
2151 total_amount: Amount::from_sat(50),
2152 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2153 }
2154 ];
2155
2156 let result = ArkoorBuilder::new_with_checkpoint(
2158 alice_vtxo.clone(),
2159 outputs.clone(),
2160 dust_outputs.clone(),
2161 );
2162 match result {
2163 Err(ArkoorConstructionError::Dust) => {},
2164 _ => panic!("Expected Dust error for isolation sum < 330"),
2165 }
2166 }
2167
2168 #[test]
2169 fn spend_dust_vtxo() {
2170 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2172 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2173 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2174
2175 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2177 amount: Amount::from_sat(200),
2178 fee: Amount::ZERO,
2179 expiry_height: 1000,
2180 exit_delta: 128,
2181 user_keypair: alice_keypair.clone(),
2182 server_keypair: server_keypair.clone()
2183 }.build();
2184
2185 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
2186
2187 let dust_outputs = vec![
2190 ArkoorDestination {
2191 total_amount: Amount::from_sat(100),
2192 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2193 },
2194 ArkoorDestination {
2195 total_amount: Amount::from_sat(100),
2196 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2197 }
2198 ];
2199
2200 let user_builder = ArkoorBuilder::new_with_checkpoint(
2201 alice_vtxo.clone(),
2202 dust_outputs,
2203 vec![],
2204 ).expect("Valid arkoor request for all-dust case");
2205
2206 assert!(
2208 user_builder.unsigned_isolation_fanout_tx.is_none(),
2209 "Dust isolation should NOT be active",
2210 );
2211
2212 assert_eq!(user_builder.outputs.len(), 2);
2214
2215 assert_eq!(user_builder.nb_sigs(), 3);
2217
2218 let user_builder = user_builder.generate_user_nonces(alice_keypair);
2220 let cosign_request = user_builder.cosign_request();
2221
2222 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
2224 .expect("Invalid cosign request")
2225 .server_cosign(&server_keypair)
2226 .expect("Incorrect key");
2227
2228 let cosign_data = server_builder.cosign_response();
2229
2230 let signed_builder = user_builder
2232 .user_cosign(&alice_keypair, &cosign_data)
2233 .expect("Valid cosign data and correct key");
2234 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
2235 let vtxos = signed_builder.build_signed_vtxos();
2236
2237 assert_eq!(vtxos.len(), 2);
2239
2240 for vtxo in vtxos.into_iter() {
2241 vtxo.validate(&funding_tx).expect("Invalid VTXO");
2243
2244 assert_eq!(vtxo.amount(), Amount::from_sat(100));
2246
2247 let mut prev_tx = funding_tx.clone();
2249 for tx in vtxo.transactions().map(|item| item.tx) {
2250 let prev_outpoint: OutPoint = tx.input[0].previous_output;
2251 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
2252 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
2253 prev_tx = tx;
2254 }
2255 }
2256 }
2257
2258 #[test]
2259 fn spend_nondust_vtxo_to_dust() {
2260 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2263 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2264 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2265
2266 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2268 amount: Amount::from_sat(500),
2269 fee: Amount::ZERO,
2270 expiry_height: 1000,
2271 exit_delta: 128,
2272 user_keypair: alice_keypair.clone(),
2273 server_keypair: server_keypair.clone()
2274 }.build();
2275
2276 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
2277
2278 let dust_outputs = vec![
2281 ArkoorDestination {
2282 total_amount: Amount::from_sat(250),
2283 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2284 },
2285 ArkoorDestination {
2286 total_amount: Amount::from_sat(250),
2287 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2288 }
2289 ];
2290
2291 let user_builder = ArkoorBuilder::new_with_checkpoint(
2292 alice_vtxo.clone(),
2293 dust_outputs,
2294 vec![],
2295 ).expect("Valid arkoor request for non-dust to dust case");
2296
2297 assert!(
2299 user_builder.unsigned_isolation_fanout_tx.is_none(),
2300 "Dust isolation should NOT be active",
2301 );
2302
2303 assert_eq!(user_builder.outputs.len(), 2);
2305
2306 assert_eq!(user_builder.nb_sigs(), 3);
2308
2309 let user_builder = user_builder.generate_user_nonces(alice_keypair);
2311 let cosign_request = user_builder.cosign_request();
2312
2313 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
2315 .expect("Invalid cosign request")
2316 .server_cosign(&server_keypair)
2317 .expect("Incorrect key");
2318
2319 let cosign_data = server_builder.cosign_response();
2320
2321 let signed_builder = user_builder
2323 .user_cosign(&alice_keypair, &cosign_data)
2324 .expect("Valid cosign data and correct key");
2325 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
2326 let vtxos = signed_builder.build_signed_vtxos();
2327
2328 assert_eq!(vtxos.len(), 2);
2330
2331 for vtxo in vtxos.into_iter() {
2332 vtxo.validate(&funding_tx).expect("Invalid VTXO");
2334
2335 assert_eq!(vtxo.amount(), Amount::from_sat(250));
2337
2338 let mut prev_tx = funding_tx.clone();
2340 for tx in vtxo.transactions().map(|item| item.tx) {
2341 let prev_outpoint: OutPoint = tx.input[0].previous_output;
2342 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
2343 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
2344 prev_tx = tx;
2345 }
2346 }
2347 }
2348
2349 #[test]
2350 fn isolate_dust_all_nondust() {
2351 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2354 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2355 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2356
2357 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2358 amount: Amount::from_sat(1000),
2359 fee: Amount::ZERO,
2360 expiry_height: 1000,
2361 exit_delta: 128,
2362 user_keypair: alice_keypair.clone(),
2363 server_keypair: server_keypair.clone()
2364 }.build();
2365
2366 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2367
2368 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2369 alice_vtxo,
2370 vec![
2371 ArkoorDestination {
2372 total_amount: Amount::from_sat(500),
2373 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2374 },
2375 ArkoorDestination {
2376 total_amount: Amount::from_sat(500),
2377 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2378 }
2379 ],
2380 ).unwrap();
2381
2382 assert!(builder.unsigned_isolation_fanout_tx.is_none());
2384
2385 assert_eq!(builder.outputs.len(), 2);
2387 assert_eq!(builder.isolated_outputs.len(), 0);
2388 }
2389
2390 #[test]
2391 fn isolate_dust_all_dust() {
2392 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2395 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2396 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2397
2398 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2399 amount: Amount::from_sat(400),
2400 fee: Amount::ZERO,
2401 expiry_height: 1000,
2402 exit_delta: 128,
2403 user_keypair: alice_keypair.clone(),
2404 server_keypair: server_keypair.clone()
2405 }.build();
2406
2407 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2408
2409 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2410 alice_vtxo,
2411 vec![
2412 ArkoorDestination {
2413 total_amount: Amount::from_sat(200),
2414 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2415 },
2416 ArkoorDestination {
2417 total_amount: Amount::from_sat(200),
2418 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2419 }
2420 ],
2421 ).unwrap();
2422
2423 assert!(builder.unsigned_isolation_fanout_tx.is_none());
2425
2426 assert_eq!(builder.outputs.len(), 2);
2428 assert_eq!(builder.isolated_outputs.len(), 0);
2429 }
2430
2431 #[test]
2432 fn isolate_dust_sufficient_dust() {
2433 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2436 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2437 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2438
2439 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2440 amount: Amount::from_sat(1000),
2441 fee: Amount::ZERO,
2442 expiry_height: 1000,
2443 exit_delta: 128,
2444 user_keypair: alice_keypair.clone(),
2445 server_keypair: server_keypair.clone()
2446 }.build();
2447
2448 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2449
2450 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2452 alice_vtxo,
2453 vec![
2454 ArkoorDestination {
2455 total_amount: Amount::from_sat(600),
2456 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2457 },
2458 ArkoorDestination {
2459 total_amount: Amount::from_sat(200),
2460 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2461 },
2462 ArkoorDestination {
2463 total_amount: Amount::from_sat(200),
2464 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2465 }
2466 ],
2467 ).unwrap();
2468
2469 assert!(builder.unsigned_isolation_fanout_tx.is_some());
2471
2472 assert_eq!(builder.outputs.len(), 1);
2474 assert_eq!(builder.isolated_outputs.len(), 2);
2475 }
2476
2477 #[test]
2478 fn isolate_dust_split_successful() {
2479 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2483 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2484 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2485
2486 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2487 amount: Amount::from_sat(1000),
2488 fee: Amount::ZERO,
2489 expiry_height: 1000,
2490 exit_delta: 128,
2491 user_keypair: alice_keypair.clone(),
2492 server_keypair: server_keypair.clone()
2493 }.build();
2494
2495 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2496
2497 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2498 alice_vtxo,
2499 vec![
2500 ArkoorDestination {
2501 total_amount: Amount::from_sat(800),
2502 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2503 },
2504 ArkoorDestination {
2505 total_amount: Amount::from_sat(100),
2506 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2507 },
2508 ArkoorDestination {
2509 total_amount: Amount::from_sat(100),
2510 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2511 }
2512 ],
2513 ).unwrap();
2514
2515 assert!(builder.unsigned_isolation_fanout_tx.is_some());
2517
2518 assert_eq!(builder.outputs.len(), 1);
2520 assert_eq!(builder.isolated_outputs.len(), 3);
2521
2522 assert_eq!(builder.outputs[0].total_amount, Amount::from_sat(670));
2524 let isolated_sum: Amount = builder.isolated_outputs.iter().map(|o| o.total_amount).sum();
2525 assert_eq!(isolated_sum, P2TR_DUST);
2526 }
2527
2528 #[test]
2529 fn isolate_dust_split_impossible() {
2530 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2535 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2536 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2537
2538 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2539 amount: Amount::from_sat(600),
2540 fee: Amount::ZERO,
2541 expiry_height: 1000,
2542 exit_delta: 128,
2543 user_keypair: alice_keypair.clone(),
2544 server_keypair: server_keypair.clone()
2545 }.build();
2546
2547 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2548
2549 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2550 alice_vtxo,
2551 vec![
2552 ArkoorDestination {
2553 total_amount: Amount::from_sat(400),
2554 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2555 },
2556 ArkoorDestination {
2557 total_amount: Amount::from_sat(100),
2558 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2559 },
2560 ArkoorDestination {
2561 total_amount: Amount::from_sat(100),
2562 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2563 }
2564 ],
2565 ).unwrap();
2566
2567 assert!(builder.unsigned_isolation_fanout_tx.is_none());
2569
2570 assert_eq!(builder.outputs.len(), 3);
2572 assert_eq!(builder.isolated_outputs.len(), 0);
2573 }
2574
2575 #[test]
2576 fn isolate_dust_exactly_boundary() {
2577 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2581 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2582 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2583
2584 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2585 amount: Amount::from_sat(1000),
2586 fee: Amount::ZERO,
2587 expiry_height: 1000,
2588 exit_delta: 128,
2589 user_keypair: alice_keypair.clone(),
2590 server_keypair: server_keypair.clone()
2591 }.build();
2592
2593 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2594
2595 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2596 alice_vtxo,
2597 vec![
2598 ArkoorDestination {
2599 total_amount: Amount::from_sat(660),
2600 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2601 },
2602 ArkoorDestination {
2603 total_amount: Amount::from_sat(170),
2604 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2605 },
2606 ArkoorDestination {
2607 total_amount: Amount::from_sat(170),
2608 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2609 }
2610 ],
2611 ).unwrap();
2612
2613 assert!(builder.unsigned_isolation_fanout_tx.is_some());
2615
2616 assert_eq!(builder.outputs.len(), 1);
2618 assert_eq!(builder.isolated_outputs.len(), 2);
2619
2620 assert_eq!(builder.outputs[0].total_amount, Amount::from_sat(660));
2622 assert_eq!(builder.isolated_outputs[0].total_amount, Amount::from_sat(170));
2623 assert_eq!(builder.isolated_outputs[1].total_amount, Amount::from_sat(170));
2624 }
2625}