1pub mod package;
68
69use std::marker::PhantomData;
70
71use bitcoin::hashes::Hash;
72use bitcoin::sighash::{self, SighashCache};
73use bitcoin::amount::CheckedSum;
74use bitcoin::{
75 Amount, OutPoint, ScriptBuf, Sequence, TapSighash, TapSighashType, Transaction, TxIn, TxOut, Txid, Witness
76};
77use bitcoin::taproot::TapTweakHash;
78use bitcoin::secp256k1::{schnorr, Keypair, PublicKey};
79use bitcoin_ext::{fee, P2TR_DUST, TxOutExt};
80use secp256k1_musig::musig::PublicNonce;
81
82use crate::{musig, scripts, Vtxo, VtxoId, ServerVtxo};
83use crate::attestations::ArkoorCosignAttestation;
84use crate::vtxo::{Full, ServerVtxoPolicy, VtxoPolicy, VtxoRef};
85use crate::vtxo::genesis::{GenesisItem, GenesisTransition};
86
87pub use package::ArkoorPackageBuilder;
88
89
90#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
91pub enum ArkoorConstructionError {
92 #[error("Input amount of {input} does not match output amount of {output}")]
93 Unbalanced {
94 input: Amount,
95 output: Amount,
96 },
97 #[error("An output is below the dust threshold")]
98 Dust,
99 #[error("At least one output is required")]
100 NoOutputs,
101 #[error("Too many outputs provided")]
102 TooManyOutputs,
103 #[error("Too many inputs provided")]
104 TooManyInputs,
105 #[error("Total amount overflowed while allocating outputs to inputs")]
106 Overflow,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
110pub enum ArkoorSigningError {
111 #[error("Invalid attestation")]
112 InvalidAttestation(AttestationError),
113 #[error("An error occurred while building arkoor: {0}")]
114 ArkoorConstructionError(ArkoorConstructionError),
115 #[error("Wrong number of user nonces provided. Expected {expected}, got {got}")]
116 InvalidNbUserNonces {
117 expected: usize,
118 got: usize,
119 },
120 #[error("Wrong number of server nonces provided. Expected {expected}, got {got}")]
121 InvalidNbServerNonces {
122 expected: usize,
123 got: usize,
124 },
125 #[error("Incorrect signing key provided. Expected {expected}, got {got}")]
126 IncorrectKey {
127 expected: PublicKey,
128 got: PublicKey,
129 },
130 #[error("Wrong number of server partial sigs. Expected {expected}, got {got}")]
131 InvalidNbServerPartialSigs {
132 expected: usize,
133 got: usize
134 },
135 #[error("Invalid partial signature at index {index}")]
136 InvalidPartialSignature {
137 index: usize,
138 },
139 #[error("Wrong number of packages. Expected {expected}, got {got}")]
140 InvalidNbPackages {
141 expected: usize,
142 got: usize,
143 },
144 #[error("Wrong number of keypairs. Expected {expected}, got {got}")]
145 InvalidNbKeypairs {
146 expected: usize,
147 got: usize,
148 },
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
156pub struct ArkoorDestination {
157 pub total_amount: Amount,
158 #[serde(with = "crate::encode::serde")]
159 pub policy: VtxoPolicy,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct ArkoorCosignResponse {
164 pub server_pub_nonces: Vec<musig::PublicNonce>,
165 pub server_partial_sigs: Vec<musig::PartialSignature>,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct ArkoorCosignRequest<V> {
170 pub user_pub_nonces: Vec<musig::PublicNonce>,
171 pub input: V,
172 pub outputs: Vec<ArkoorDestination>,
173 pub isolated_outputs: Vec<ArkoorDestination>,
174 pub use_checkpoint: bool,
175 pub attestation: ArkoorCosignAttestation,
176}
177
178impl<V> ArkoorCosignRequest<V> {
179 pub fn new_with_attestation(
180 user_pub_nonces: Vec<musig::PublicNonce>,
181 input: V,
182 outputs: Vec<ArkoorDestination>,
183 isolated_outputs: Vec<ArkoorDestination>,
184 use_checkpoint: bool,
185 attestation: ArkoorCosignAttestation,
186 ) -> Self {
187 Self {
188 user_pub_nonces,
189 input,
190 outputs,
191 isolated_outputs,
192 use_checkpoint,
193 attestation,
194 }
195 }
196
197 pub fn all_outputs(&self) -> impl Iterator<Item = &ArkoorDestination> + Clone {
198 self.outputs.iter().chain(&self.isolated_outputs)
199 }
200}
201
202impl<V: VtxoRef> ArkoorCosignRequest<V> {
203 pub fn new(
204 user_pub_nonces: Vec<musig::PublicNonce>,
205 input: V,
206 outputs: Vec<ArkoorDestination>,
207 isolated_outputs: Vec<ArkoorDestination>,
208 use_checkpoint: bool,
209 keypair: &Keypair,
210 ) -> Self {
211 let all_outputs = &outputs.iter().chain(&isolated_outputs).collect::<Vec<_>>();
212 let attestation = ArkoorCosignAttestation::new(input.vtxo_id(), all_outputs, keypair);
213
214 Self::new_with_attestation(
215 user_pub_nonces,
216 input,
217 outputs,
218 isolated_outputs,
219 use_checkpoint,
220 attestation,
221 )
222 }
223}
224
225impl ArkoorCosignRequest<VtxoId> {
226 pub fn with_vtxo(self, vtxo: Vtxo<Full>) -> Result<ArkoorCosignRequest<Vtxo<Full>>, &'static str> {
227 if self.input != vtxo.id() {
228 return Err("Input vtxo id does not match the provided vtxo id")
229 }
230
231 Ok(ArkoorCosignRequest::new_with_attestation(
232 self.user_pub_nonces,
233 vtxo,
234 self.outputs,
235 self.isolated_outputs,
236 self.use_checkpoint,
237 self.attestation,
238 ))
239 }
240}
241
242#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Hash)]
243#[error("invalid attestation")]
244pub struct AttestationError;
245
246impl ArkoorCosignRequest<Vtxo> {
247 pub fn verify_attestation(&self) -> Result<(), AttestationError> {
248 let outputs = self.all_outputs().collect::<Vec<_>>();
249 self.attestation.verify(&self.input, &outputs)
250 .map_err(|_| AttestationError)
251 }
252}
253
254pub mod state {
255 mod sealed {
263 pub trait Sealed {}
264 impl Sealed for super::Initial {}
265 impl Sealed for super::UserGeneratedNonces {}
266 impl Sealed for super::UserSigned {}
267 impl Sealed for super::ServerCanCosign {}
268 impl Sealed for super::ServerSigned {}
269 }
270
271 pub trait BuilderState: sealed::Sealed {}
272
273 pub struct Initial;
275 impl BuilderState for Initial {}
276
277 pub struct UserGeneratedNonces;
279 impl BuilderState for UserGeneratedNonces {}
280
281 pub struct UserSigned;
283 impl BuilderState for UserSigned {}
284
285 pub struct ServerCanCosign;
287 impl BuilderState for ServerCanCosign {}
288
289
290 pub struct ServerSigned;
292 impl BuilderState for ServerSigned {}
293}
294
295pub struct ArkoorBuilder<S: state::BuilderState> {
296 input: Vtxo<Full>,
299 outputs: Vec<ArkoorDestination>,
301 isolated_outputs: Vec<ArkoorDestination>,
305
306 checkpoint_data: Option<(Transaction, Txid)>,
311 unsigned_arkoor_txs: Vec<Transaction>,
313 unsigned_isolation_fanout_tx: Option<Transaction>,
316 sighashes: Vec<TapSighash>,
318 input_tweak: TapTweakHash,
320 checkpoint_policy_tweak: TapTweakHash,
323 new_vtxo_ids: Vec<VtxoId>,
325
326 user_keypair: Option<Keypair>,
331 user_pub_nonces: Option<Vec<musig::PublicNonce>>,
333 user_sec_nonces: Option<Vec<musig::SecretNonce>>,
335 server_pub_nonces: Option<Vec<musig::PublicNonce>>,
337 server_partial_sigs: Option<Vec<musig::PartialSignature>>,
339 full_signatures: Option<Vec<schnorr::Signature>>,
341
342 _state: PhantomData<S>,
343}
344
345impl<S: state::BuilderState> ArkoorBuilder<S> {
346 pub fn input(&self) -> &Vtxo<Full> {
348 &self.input
349 }
350
351 pub fn normal_outputs(&self) -> &[ArkoorDestination] {
353 &self.outputs
354 }
355
356 pub fn isolated_outputs(&self) -> &[ArkoorDestination] {
358 &self.isolated_outputs
359 }
360
361 pub fn all_outputs(
363 &self,
364 ) -> impl Iterator<Item = &ArkoorDestination> + Clone {
365 self.outputs.iter().chain(&self.isolated_outputs)
366 }
367
368 fn build_checkpoint_vtxo_at(
369 &self,
370 output_idx: usize,
371 checkpoint_sig: Option<schnorr::Signature>
372 ) -> ServerVtxo<Full> {
373 let output = &self.outputs[output_idx];
374 let (checkpoint_tx, checkpoint_txid) = self.checkpoint_data.as_ref()
375 .expect("called checkpoint_vtxo_at in context without checkpoints");
376
377 Vtxo {
378 amount: output.total_amount,
379 policy: ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey()),
380 expiry_height: self.input.expiry_height,
381 server_pubkey: self.input.server_pubkey,
382 exit_delta: self.input.exit_delta,
383 point: OutPoint::new(*checkpoint_txid, u32::try_from(output_idx).expect("output index fits in u32")),
384 anchor_point: self.input.anchor_point,
385 genesis: Full {
386 items: self.input.genesis.items.clone().into_iter().chain([
387 GenesisItem {
388 transition: GenesisTransition::new_arkoor(
389 vec![self.input.user_pubkey()],
390 self.input.policy().taproot(
391 self.input.server_pubkey,
392 self.input.exit_delta,
393 self.input.expiry_height,
394 ).tap_tweak(),
395 checkpoint_sig,
396 ),
397 output_idx: u8::try_from(output_idx).expect("arkoor output index fits in u8"),
398 other_outputs: checkpoint_tx.output
399 .iter().enumerate()
400 .filter_map(|(i, txout)| {
401 if i == output_idx || txout.is_p2a_fee_anchor() {
402 None
403 } else {
404 Some(txout.clone())
405 }
406 })
407 .collect(),
408 fee_amount: Amount::ZERO,
409 },
410 ]).collect(),
411 },
412 }
413 }
414
415 fn build_vtxo_at(
416 &self,
417 output_idx: usize,
418 checkpoint_sig: Option<schnorr::Signature>,
419 arkoor_sig: Option<schnorr::Signature>,
420 ) -> Vtxo<Full> {
421 let output = &self.outputs[output_idx];
422
423 if let Some((checkpoint_tx, _txid)) = &self.checkpoint_data {
424 let checkpoint_policy = ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey());
426
427 Vtxo {
428 amount: output.total_amount,
429 policy: output.policy.clone(),
430 expiry_height: self.input.expiry_height,
431 server_pubkey: self.input.server_pubkey,
432 exit_delta: self.input.exit_delta,
433 point: self.new_vtxo_ids[output_idx].to_point(),
434 anchor_point: self.input.anchor_point,
435 genesis: Full {
436 items: self.input.genesis.items.iter().cloned().chain([
437 GenesisItem {
438 transition: GenesisTransition::new_arkoor(
439 vec![self.input.user_pubkey()],
440 self.input.policy.taproot(
441 self.input.server_pubkey,
442 self.input.exit_delta,
443 self.input.expiry_height,
444 ).tap_tweak(),
445 checkpoint_sig,
446 ),
447 output_idx: u8::try_from(output_idx).expect("arkoor output index fits in u8"),
448 other_outputs: checkpoint_tx.output
449 .iter().enumerate()
450 .filter_map(|(i, txout)| {
451 if i == output_idx || txout.is_p2a_fee_anchor() {
452 None
453 } else {
454 Some(txout.clone())
455 }
456 })
457 .collect(),
458 fee_amount: Amount::ZERO,
459 },
460 GenesisItem {
461 transition: GenesisTransition::new_arkoor(
462 vec![self.input.user_pubkey()],
463 checkpoint_policy.taproot(
464 self.input.server_pubkey,
465 self.input.exit_delta,
466 self.input.expiry_height,
467 ).tap_tweak(),
468 arkoor_sig,
469 ),
470 output_idx: 0,
471 other_outputs: vec![],
472 fee_amount: Amount::ZERO,
473 }
474 ]).collect(),
475 },
476 }
477 } else {
478 let arkoor_tx = &self.unsigned_arkoor_txs[0];
480
481 Vtxo {
482 amount: output.total_amount,
483 policy: output.policy.clone(),
484 expiry_height: self.input.expiry_height,
485 server_pubkey: self.input.server_pubkey,
486 exit_delta: self.input.exit_delta,
487 point: OutPoint::new(arkoor_tx.compute_txid(), u32::try_from(output_idx).expect("output index fits in u32")),
488 anchor_point: self.input.anchor_point,
489 genesis: Full {
490 items: self.input.genesis.items.iter().cloned().chain([
491 GenesisItem {
492 transition: GenesisTransition::new_arkoor(
493 vec![self.input.user_pubkey()],
494 self.input.policy.taproot(
495 self.input.server_pubkey,
496 self.input.exit_delta,
497 self.input.expiry_height,
498 ).tap_tweak(),
499 arkoor_sig,
500 ),
501 output_idx: u8::try_from(output_idx).expect("arkoor output index fits in u8"),
502 other_outputs: arkoor_tx.output
503 .iter().enumerate()
504 .filter_map(|(idx, txout)| {
505 if idx == output_idx || txout.is_p2a_fee_anchor() {
506 None
507 } else {
508 Some(txout.clone())
509 }
510 })
511 .collect(),
512 fee_amount: Amount::ZERO,
513 }
514 ]).collect(),
515 },
516 }
517 }
518 }
519
520 fn build_isolated_vtxo_at(
528 &self,
529 isolated_idx: usize,
530 pre_fanout_tx_sig: Option<schnorr::Signature>,
531 isolation_fanout_tx_sig: Option<schnorr::Signature>,
532 ) -> Vtxo<Full> {
533 let output = &self.isolated_outputs[isolated_idx];
534 let checkpoint_policy = ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey());
535
536 let fanout_tx = self.unsigned_isolation_fanout_tx.as_ref()
537 .expect("construct_dust_vtxo_at called without dust isolation");
538
539 let dust_isolation_output_idx = self.outputs.len();
541
542 if let Some((checkpoint_tx, _txid)) = &self.checkpoint_data {
543 Vtxo {
545 amount: output.total_amount,
546 policy: output.policy.clone(),
547 expiry_height: self.input.expiry_height,
548 server_pubkey: self.input.server_pubkey,
549 exit_delta: self.input.exit_delta,
550 point: OutPoint::new(fanout_tx.compute_txid(), u32::try_from(isolated_idx).expect("output index fits in u32")),
551 anchor_point: self.input.anchor_point,
552 genesis: Full {
553 items: self.input.genesis.items.iter().cloned().chain([
554 GenesisItem {
556 transition: GenesisTransition::new_arkoor(
557 vec![self.input.user_pubkey()],
558 self.input.policy.taproot(
559 self.input.server_pubkey,
560 self.input.exit_delta,
561 self.input.expiry_height,
562 ).tap_tweak(),
563 pre_fanout_tx_sig,
564 ),
565 output_idx: u8::try_from(dust_isolation_output_idx).expect("arkoor output index fits in u8"),
566 other_outputs: checkpoint_tx.output
569 .iter().enumerate()
570 .filter_map(|(idx, txout)| {
571 let is_p2a = txout.is_p2a_fee_anchor();
572 if idx == dust_isolation_output_idx || is_p2a {
573 None
574 } else {
575 Some(txout.clone())
576 }
577 })
578 .collect(),
579 fee_amount: Amount::ZERO,
580 },
581 GenesisItem {
583 transition: GenesisTransition::new_arkoor(
584 vec![self.input.user_pubkey()],
585 checkpoint_policy.taproot(
586 self.input.server_pubkey,
587 self.input.exit_delta,
588 self.input.expiry_height,
589 ).tap_tweak(),
590 isolation_fanout_tx_sig,
591 ),
592 output_idx: u8::try_from(isolated_idx).expect("arkoor output index fits in u8"),
593 other_outputs: fanout_tx.output
596 .iter().enumerate()
597 .filter_map(|(idx, txout)| {
598 if idx == isolated_idx || txout.is_p2a_fee_anchor() {
599 None
600 } else {
601 Some(txout.clone())
602 }
603 })
604 .collect(),
605 fee_amount: Amount::ZERO,
606 },
607 ]).collect(),
608 },
609 }
610 } else {
611 let arkoor_tx = &self.unsigned_arkoor_txs[0];
613
614 Vtxo {
615 amount: output.total_amount,
616 policy: output.policy.clone(),
617 expiry_height: self.input.expiry_height,
618 server_pubkey: self.input.server_pubkey,
619 exit_delta: self.input.exit_delta,
620 point: OutPoint::new(fanout_tx.compute_txid(), u32::try_from(isolated_idx).expect("output index fits in u32")),
621 anchor_point: self.input.anchor_point,
622 genesis: Full {
623 items: self.input.genesis.items.iter().cloned().chain([
624 GenesisItem {
626 transition: GenesisTransition::new_arkoor(
627 vec![self.input.user_pubkey()],
628 self.input.policy.taproot(
629 self.input.server_pubkey,
630 self.input.exit_delta,
631 self.input.expiry_height,
632 ).tap_tweak(),
633 pre_fanout_tx_sig,
634 ),
635 output_idx: u8::try_from(dust_isolation_output_idx).expect("arkoor output index fits in u8"),
636 other_outputs: arkoor_tx.output
637 .iter().enumerate()
638 .filter_map(|(idx, txout)| {
639 if idx == dust_isolation_output_idx || txout.is_p2a_fee_anchor() {
640 None
641 } else {
642 Some(txout.clone())
643 }
644 })
645 .collect(),
646 fee_amount: Amount::ZERO,
647 },
648 GenesisItem {
650 transition: GenesisTransition::new_arkoor(
651 vec![self.input.user_pubkey()],
652 checkpoint_policy.taproot(
653 self.input.server_pubkey,
654 self.input.exit_delta,
655 self.input.expiry_height,
656 ).tap_tweak(),
657 isolation_fanout_tx_sig,
658 ),
659 output_idx: u8::try_from(isolated_idx).expect("arkoor output index fits in u8"),
660 other_outputs: fanout_tx.output
661 .iter().enumerate()
662 .filter_map(|(idx, txout)| {
663 if idx == isolated_idx || txout.is_p2a_fee_anchor() {
664 None
665 } else {
666 Some(txout.clone())
667 }
668 })
669 .collect(),
670 fee_amount: Amount::ZERO,
671 },
672 ]).collect(),
673 },
674 }
675 }
676 }
677
678 fn nb_sigs(&self) -> usize {
679 let base = if self.checkpoint_data.is_some() {
680 self.outputs.len().saturating_add(1) } else {
682 1 };
684
685 if self.unsigned_isolation_fanout_tx.is_some() {
686 base.saturating_add(1) } else {
688 base
689 }
690 }
691
692 pub fn build_unsigned_vtxos<'a>(&'a self) -> impl Iterator<Item = Vtxo<Full>> + 'a {
693 let regular = (0..self.outputs.len()).map(|i| self.build_vtxo_at(i, None, None));
694 let isolated = (0..self.isolated_outputs.len())
695 .map(|i| self.build_isolated_vtxo_at(i, None, None));
696 regular.chain(isolated)
697 }
698
699 fn build_internal_vtxos(
705 &self,
706 intermediate_sig: Option<schnorr::Signature>,
707 ) -> Vec<(ServerVtxo<Full>, Txid)> {
708 let mut ret = Vec::new();
709
710 if self.checkpoint_data.is_some() {
711 for idx in 0..self.outputs.len() {
712 let vtxo = self.build_checkpoint_vtxo_at(idx, intermediate_sig);
713 let spending_txid = self.unsigned_arkoor_txs[idx].compute_txid();
714 ret.push((vtxo, spending_txid));
715 }
716 }
717
718 if !self.isolated_outputs.is_empty() {
719 let output_idx = self.outputs.len();
720
721 let (int_tx, int_txid) = if let Some((tx, txid)) = &self.checkpoint_data {
722 (tx, *txid)
723 } else {
724 let arkoor_tx = &self.unsigned_arkoor_txs[0];
725 (arkoor_tx, arkoor_tx.compute_txid())
726 };
727
728 let vtxo = Vtxo {
729 amount: self.isolated_outputs.iter().map(|o| o.total_amount).sum(),
730 policy: ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey()),
731 expiry_height: self.input.expiry_height,
732 server_pubkey: self.input.server_pubkey,
733 exit_delta: self.input.exit_delta,
734 point: OutPoint::new(int_txid, u32::try_from(output_idx).expect("output index fits in u32")),
735 anchor_point: self.input.anchor_point,
736 genesis: Full {
737 items: self.input.genesis.items.clone().into_iter().chain([
738 GenesisItem {
739 transition: GenesisTransition::new_arkoor(
740 vec![self.input.user_pubkey()],
741 self.input_tweak,
742 intermediate_sig,
743 ),
744 output_idx: u8::try_from(output_idx).expect("arkoor output index fits in u8"),
745 other_outputs: int_tx.output.iter().enumerate()
746 .filter_map(|(i, txout)| {
747 if i == output_idx || txout.is_p2a_fee_anchor() {
748 None
749 } else {
750 Some(txout.clone())
751 }
752 })
753 .collect(),
754 fee_amount: Amount::ZERO,
755 },
756 ]).collect(),
757 },
758 };
759
760 let spending_txid = self.unsigned_isolation_fanout_tx.as_ref()
761 .expect("isolation fanout tx must exist when isolated_outputs is non-empty")
762 .compute_txid();
763 ret.push((vtxo, spending_txid));
764 }
765
766 ret
767 }
768
769 pub fn input_spend_info(&self) -> (VtxoId, Txid) {
771 if let Some((_tx, checkpoint_txid)) = &self.checkpoint_data {
772 (self.input.id(), *checkpoint_txid)
773 } else {
774 (self.input.id(), self.unsigned_arkoor_txs[0].compute_txid())
775 }
776 }
777
778 pub fn build_unsigned_internal_vtxos(&self) -> Vec<(ServerVtxo<Full>, Txid)> {
781 self.build_internal_vtxos(None)
782 }
783
784 pub fn spend_info(&self) -> Vec<(VtxoId, Txid)> {
786 let mut ret = vec![self.input_spend_info()];
787 for (vtxo, spending_txid) in self.build_unsigned_internal_vtxos() {
788 ret.push((vtxo.id(), spending_txid));
789 }
790 ret
791 }
792
793 pub fn virtual_transactions(&self) -> Vec<Txid> {
798 let mut ret = Vec::new();
799 if let Some((_, txid)) = &self.checkpoint_data {
801 ret.push(*txid);
802 }
803 ret.extend(self.unsigned_arkoor_txs.iter().map(|tx| tx.compute_txid()));
805 if let Some(tx) = &self.unsigned_isolation_fanout_tx {
807 ret.push(tx.compute_txid());
808 }
809 ret
810 }
811
812 fn taptweak_at(&self, idx: usize) -> TapTweakHash {
813 if idx == 0 { self.input_tweak } else { self.checkpoint_policy_tweak }
814 }
815
816 fn user_pubkey(&self) -> PublicKey {
817 self.input.user_pubkey()
818 }
819
820 fn server_pubkey(&self) -> PublicKey {
821 self.input.server_pubkey()
822 }
823
824 fn construct_unsigned_checkpoint_tx<G>(
829 input: &Vtxo<G>,
830 outputs: &[ArkoorDestination],
831 dust_isolation_amount: Option<Amount>,
832 ) -> Transaction {
833
834 let output_policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
836 let checkpoint_spk = output_policy
837 .script_pubkey(input.server_pubkey(), input.exit_delta(), input.expiry_height());
838
839 Transaction {
840 version: bitcoin::transaction::Version(3),
841 lock_time: bitcoin::absolute::LockTime::ZERO,
842 input: vec![TxIn {
843 previous_output: input.point(),
844 script_sig: ScriptBuf::new(),
845 sequence: Sequence::ZERO,
846 witness: Witness::new(),
847 }],
848 output: outputs.iter().map(|o| {
849 TxOut {
850 value: o.total_amount,
851 script_pubkey: checkpoint_spk.clone(),
852 }
853 })
854 .chain(dust_isolation_amount.map(|amt| {
856 TxOut {
857 value: amt,
858 script_pubkey: checkpoint_spk.clone(),
859 }
860 }))
861 .chain([fee::fee_anchor()]).collect()
862 }
863 }
864
865 fn construct_unsigned_arkoor_txs<G>(
866 input: &Vtxo<G>,
867 outputs: &[ArkoorDestination],
868 checkpoint_txid: Option<Txid>,
869 dust_isolation_amount: Option<Amount>,
870 ) -> Vec<Transaction> {
871
872 if let Some(checkpoint_txid) = checkpoint_txid {
873 let mut arkoor_txs = Vec::with_capacity(outputs.len());
875
876 for (vout, output) in outputs.iter().enumerate() {
877 let transaction = Transaction {
878 version: bitcoin::transaction::Version(3),
879 lock_time: bitcoin::absolute::LockTime::ZERO,
880 input: vec![TxIn {
881 previous_output: OutPoint::new(checkpoint_txid, u32::try_from(vout).expect("output index fits in u32")),
882 script_sig: ScriptBuf::new(),
883 sequence: Sequence::ZERO,
884 witness: Witness::new(),
885 }],
886 output: vec![
887 output.policy.txout(
888 output.total_amount,
889 input.server_pubkey(),
890 input.exit_delta(),
891 input.expiry_height(),
892 ),
893 fee::fee_anchor(),
894 ]
895 };
896 arkoor_txs.push(transaction);
897 }
898
899 arkoor_txs
900 } else {
901 let checkpoint_policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
903 let checkpoint_spk = checkpoint_policy.script_pubkey(
904 input.server_pubkey(),
905 input.exit_delta(),
906 input.expiry_height()
907 );
908
909 let transaction = Transaction {
910 version: bitcoin::transaction::Version(3),
911 lock_time: bitcoin::absolute::LockTime::ZERO,
912 input: vec![TxIn {
913 previous_output: input.point(),
914 script_sig: ScriptBuf::new(),
915 sequence: Sequence::ZERO,
916 witness: Witness::new(),
917 }],
918 output: outputs.iter()
919 .map(|o| o.policy.txout(
920 o.total_amount,
921 input.server_pubkey(),
922 input.exit_delta(),
923 input.expiry_height(),
924 ))
925 .chain(dust_isolation_amount.map(|amt| TxOut {
927 value: amt,
928 script_pubkey: checkpoint_spk.clone(),
929 }))
930 .chain([fee::fee_anchor()])
931 .collect()
932 };
933 vec![transaction]
934 }
935 }
936
937 fn construct_unsigned_isolation_fanout_tx<G>(
945 input: &Vtxo<G>,
946 isolated_outputs: &[ArkoorDestination],
947 parent_txid: Txid, dust_isolation_output_vout: u32, ) -> Transaction {
950
951 Transaction {
952 version: bitcoin::transaction::Version(3),
953 lock_time: bitcoin::absolute::LockTime::ZERO,
954 input: vec![TxIn {
955 previous_output: OutPoint::new(parent_txid, dust_isolation_output_vout),
956 script_sig: ScriptBuf::new(),
957 sequence: Sequence::ZERO,
958 witness: Witness::new(),
959 }],
960 output: isolated_outputs.iter().map(|o| {
961 TxOut {
962 value: o.total_amount,
963 script_pubkey: o.policy.script_pubkey(
964 input.server_pubkey(),
965 input.exit_delta(),
966 input.expiry_height(),
967 ),
968 }
969 }).chain([fee::fee_anchor()]).collect(),
970 }
971 }
972
973 fn validate_amounts<G>(
974 input: &Vtxo<G>,
975 outputs: &[ArkoorDestination],
976 isolation_outputs: &[ArkoorDestination],
977 ) -> Result<(), ArkoorConstructionError> {
978
979 let input_amount = input.amount();
984
985 let output_amount = outputs.iter().chain(isolation_outputs.iter())
987 .map(|o| o.total_amount)
988 .checked_sum()
989 .ok_or(ArkoorConstructionError::Overflow)?;
990
991 if input_amount != output_amount {
992 return Err(ArkoorConstructionError::Unbalanced {
993 input: input_amount,
994 output: output_amount,
995 })
996 }
997
998 if outputs.is_empty() {
1000 return Err(ArkoorConstructionError::NoOutputs)
1001 }
1002
1003 if outputs.len() > u8::MAX as usize || isolation_outputs.len() > u8::MAX as usize {
1005 return Err(ArkoorConstructionError::TooManyOutputs)
1006 }
1007
1008 if !isolation_outputs.is_empty() {
1010 let isolation_sum: Amount = isolation_outputs.iter()
1011 .map(|o| o.total_amount).sum();
1012 if isolation_sum < P2TR_DUST {
1013 return Err(ArkoorConstructionError::Dust)
1014 }
1015 }
1016
1017 Ok(())
1018 }
1019
1020
1021 fn to_state<S2: state::BuilderState>(self) -> ArkoorBuilder<S2> {
1022 ArkoorBuilder {
1023 input: self.input,
1024 outputs: self.outputs,
1025 isolated_outputs: self.isolated_outputs,
1026 checkpoint_data: self.checkpoint_data,
1027 unsigned_arkoor_txs: self.unsigned_arkoor_txs,
1028 unsigned_isolation_fanout_tx: self.unsigned_isolation_fanout_tx,
1029 new_vtxo_ids: self.new_vtxo_ids,
1030 sighashes: self.sighashes,
1031 input_tweak: self.input_tweak,
1032 checkpoint_policy_tweak: self.checkpoint_policy_tweak,
1033 user_keypair: self.user_keypair,
1034 user_pub_nonces: self.user_pub_nonces,
1035 user_sec_nonces: self.user_sec_nonces,
1036 server_pub_nonces: self.server_pub_nonces,
1037 server_partial_sigs: self.server_partial_sigs,
1038 full_signatures: self.full_signatures,
1039 _state: PhantomData,
1040 }
1041 }
1042}
1043
1044impl ArkoorBuilder<state::Initial> {
1045 pub fn new_with_checkpoint(
1047 input: Vtxo<Full>,
1048 outputs: Vec<ArkoorDestination>,
1049 isolated_outputs: Vec<ArkoorDestination>,
1050 ) -> Result<Self, ArkoorConstructionError> {
1051 Self::new(input, outputs, isolated_outputs, true)
1052 }
1053
1054 pub fn new_without_checkpoint(
1056 input: Vtxo<Full>,
1057 outputs: Vec<ArkoorDestination>,
1058 isolated_outputs: Vec<ArkoorDestination>,
1059 ) -> Result<Self, ArkoorConstructionError> {
1060 Self::new(input, outputs, isolated_outputs, false)
1061 }
1062
1063 pub fn new_with_checkpoint_isolate_dust(
1068 input: Vtxo<Full>,
1069 outputs: Vec<ArkoorDestination>,
1070 ) -> Result<Self, ArkoorConstructionError> {
1071 Self::new_isolate_dust(input, outputs, true)
1072 }
1073
1074 pub(crate) fn new_isolate_dust(
1075 input: Vtxo<Full>,
1076 outputs: Vec<ArkoorDestination>,
1077 use_checkpoints: bool,
1078 ) -> Result<Self, ArkoorConstructionError> {
1079 if outputs.iter().all(|v| v.total_amount >= P2TR_DUST)
1081 || outputs.iter().all(|v| v.total_amount < P2TR_DUST)
1082 {
1083 return Self::new(input, outputs, vec![], use_checkpoints);
1084 }
1085
1086 let (mut dust, mut non_dust) = outputs.iter().cloned()
1088 .partition::<Vec<_>, _>(|v| v.total_amount < P2TR_DUST);
1089
1090 let dust_sum = dust.iter().map(|o| o.total_amount).sum::<Amount>();
1091 if dust_sum >= P2TR_DUST {
1092 return Self::new(input, non_dust, dust, use_checkpoints);
1093 }
1094
1095 let non_dust_sum = non_dust.iter().map(|o| o.total_amount).sum::<Amount>();
1097 if non_dust_sum < P2TR_DUST * 2 {
1098 return Self::new(input, outputs, vec![], use_checkpoints);
1099 }
1100
1101 let deficit = P2TR_DUST - dust_sum;
1103 let split_idx = non_dust.iter()
1106 .position(|o| o.total_amount - deficit >= P2TR_DUST);
1107
1108 if let Some(idx) = split_idx {
1109 let output_to_split = non_dust[idx].clone();
1110
1111 let dust_piece = ArkoorDestination {
1112 total_amount: deficit,
1113 policy: output_to_split.policy.clone(),
1114 };
1115 let leftover = ArkoorDestination {
1116 total_amount: output_to_split.total_amount - deficit,
1117 policy: output_to_split.policy,
1118 };
1119
1120 non_dust[idx] = leftover;
1121 dust.insert(0, dust_piece);
1123
1124 return Self::new(input, non_dust, dust, use_checkpoints);
1125 } else {
1126 let all_outputs = non_dust.into_iter().chain(dust).collect();
1128 return Self::new(input, all_outputs, vec![], use_checkpoints);
1129 }
1130 }
1131
1132 pub(crate) fn new(
1133 input: Vtxo<Full>,
1134 outputs: Vec<ArkoorDestination>,
1135 isolated_outputs: Vec<ArkoorDestination>,
1136 use_checkpoint: bool,
1137 ) -> Result<Self, ArkoorConstructionError> {
1138 Self::validate_amounts(&input, &outputs, &isolated_outputs)?;
1140
1141 let combined_dust_amount = if !isolated_outputs.is_empty() {
1143 Some(isolated_outputs.iter().map(|o| o.total_amount).sum())
1144 } else {
1145 None
1146 };
1147
1148 let unsigned_checkpoint_tx = if use_checkpoint {
1150 let tx = Self::construct_unsigned_checkpoint_tx(
1151 &input,
1152 &outputs,
1153 combined_dust_amount,
1154 );
1155 let txid = tx.compute_txid();
1156 Some((tx, txid))
1157 } else {
1158 None
1159 };
1160
1161 let unsigned_arkoor_txs = Self::construct_unsigned_arkoor_txs(
1163 &input,
1164 &outputs,
1165 unsigned_checkpoint_tx.as_ref().map(|t| t.1),
1166 combined_dust_amount,
1167 );
1168
1169 let unsigned_isolation_fanout_tx = if !isolated_outputs.is_empty() {
1171 let dust_isolation_output_vout = u32::try_from(outputs.len())
1174 .expect("output count fits in u32");
1175
1176 let parent_txid = if let Some((_tx, txid)) = &unsigned_checkpoint_tx {
1177 *txid
1178 } else {
1179 unsigned_arkoor_txs[0].compute_txid()
1180 };
1181
1182 Some(Self::construct_unsigned_isolation_fanout_tx(
1183 &input,
1184 &isolated_outputs,
1185 parent_txid,
1186 dust_isolation_output_vout,
1187 ))
1188 } else {
1189 None
1190 };
1191
1192 let new_vtxo_ids = unsigned_arkoor_txs.iter()
1194 .map(|tx| OutPoint::new(tx.compute_txid(), 0))
1195 .map(|outpoint| VtxoId::from(outpoint))
1196 .collect();
1197
1198 let mut sighashes = Vec::new();
1200
1201 if let Some((checkpoint_tx, _txid)) = &unsigned_checkpoint_tx {
1202 sighashes.push(arkoor_sighash(&input.txout(), checkpoint_tx));
1204
1205 for vout in 0..outputs.len() {
1207 let prevout = checkpoint_tx.output[vout].clone();
1208 sighashes.push(arkoor_sighash(&prevout, &unsigned_arkoor_txs[vout]));
1209 }
1210 } else {
1211 sighashes.push(arkoor_sighash(&input.txout(), &unsigned_arkoor_txs[0]));
1213 }
1214
1215 if let Some(ref tx) = unsigned_isolation_fanout_tx {
1217 let dust_output_vout = outputs.len(); let prevout = if let Some((checkpoint_tx, _txid)) = &unsigned_checkpoint_tx {
1219 checkpoint_tx.output[dust_output_vout].clone()
1220 } else {
1221 unsigned_arkoor_txs[0].output[dust_output_vout].clone()
1223 };
1224 sighashes.push(arkoor_sighash(&prevout, tx));
1225 }
1226
1227 let policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
1229 let input_tweak = input.output_taproot().tap_tweak();
1230 let checkpoint_policy_tweak = policy.taproot(
1231 input.server_pubkey(),
1232 input.exit_delta(),
1233 input.expiry_height(),
1234 ).tap_tweak();
1235
1236 Ok(Self {
1237 input: input,
1238 outputs: outputs,
1239 isolated_outputs,
1240 sighashes: sighashes,
1241 input_tweak,
1242 checkpoint_policy_tweak,
1243 checkpoint_data: unsigned_checkpoint_tx,
1244 unsigned_arkoor_txs: unsigned_arkoor_txs,
1245 unsigned_isolation_fanout_tx,
1246 new_vtxo_ids: new_vtxo_ids,
1247 user_keypair: None,
1248 user_pub_nonces: None,
1249 user_sec_nonces: None,
1250 server_pub_nonces: None,
1251 server_partial_sigs: None,
1252 full_signatures: None,
1253 _state: PhantomData,
1254 })
1255 }
1256
1257 pub fn generate_user_nonces(
1260 mut self,
1261 user_keypair: Keypair,
1262 ) -> ArkoorBuilder<state::UserGeneratedNonces> {
1263 let mut user_pub_nonces = Vec::with_capacity(self.nb_sigs());
1264 let mut user_sec_nonces = Vec::with_capacity(self.nb_sigs());
1265
1266 for idx in 0..self.nb_sigs() {
1267 let sighash = &self.sighashes[idx].to_byte_array();
1268 let (sec_nonce, pub_nonce) = musig::nonce_pair_with_msg(&user_keypair, sighash);
1269
1270 user_pub_nonces.push(pub_nonce);
1271 user_sec_nonces.push(sec_nonce);
1272 }
1273
1274 self.user_keypair = Some(user_keypair);
1275 self.user_pub_nonces = Some(user_pub_nonces);
1276 self.user_sec_nonces = Some(user_sec_nonces);
1277
1278 self.to_state::<state::UserGeneratedNonces>()
1279 }
1280
1281 fn set_user_pub_nonces(
1288 mut self,
1289 user_pub_nonces: Vec<musig::PublicNonce>,
1290 ) -> Result<ArkoorBuilder<state::ServerCanCosign>, ArkoorSigningError> {
1291 if user_pub_nonces.len() != self.nb_sigs() {
1292 return Err(ArkoorSigningError::InvalidNbUserNonces {
1293 expected: self.nb_sigs(),
1294 got: user_pub_nonces.len()
1295 })
1296 }
1297
1298 self.user_pub_nonces = Some(user_pub_nonces);
1299 Ok(self.to_state::<state::ServerCanCosign>())
1300 }
1301
1302 pub fn cosign_both(
1307 mut self,
1308 user_keypair: &Keypair,
1309 server_keypair: &Keypair,
1310 ) -> Result<ArkoorBuilder<state::UserSigned>, ArkoorSigningError> {
1311 if user_keypair.public_key() != self.input.user_pubkey() {
1312 return Err(ArkoorSigningError::IncorrectKey {
1313 expected: self.input.user_pubkey(),
1314 got: user_keypair.public_key(),
1315 });
1316 }
1317 if server_keypair.public_key() != self.input.server_pubkey() {
1318 return Err(ArkoorSigningError::IncorrectKey {
1319 expected: self.input.server_pubkey(),
1320 got: server_keypair.public_key(),
1321 });
1322 }
1323
1324 let mut sigs = Vec::with_capacity(self.nb_sigs());
1325 for idx in 0..self.nb_sigs() {
1326 sigs.push(musig::cosign_both(
1327 user_keypair,
1328 server_keypair,
1329 self.sighashes[idx].to_byte_array(),
1330 Some(self.taptweak_at(idx).to_byte_array()),
1331 ));
1332 }
1333
1334 self.full_signatures = Some(sigs);
1335 Ok(self.to_state::<state::UserSigned>())
1336 }
1337}
1338
1339impl<'a> ArkoorBuilder<state::ServerCanCosign> {
1340 pub fn from_cosign_request(
1341 cosign_request: ArkoorCosignRequest<Vtxo<Full>>,
1342 ) -> Result<ArkoorBuilder<state::ServerCanCosign>, ArkoorSigningError> {
1343 cosign_request.verify_attestation()
1344 .map_err(ArkoorSigningError::InvalidAttestation)?;
1345
1346 let ret = ArkoorBuilder::new(
1347 cosign_request.input,
1348 cosign_request.outputs,
1349 cosign_request.isolated_outputs,
1350 cosign_request.use_checkpoint,
1351 )
1352 .map_err(ArkoorSigningError::ArkoorConstructionError)?
1353 .set_user_pub_nonces(cosign_request.user_pub_nonces.clone())?;
1354 Ok(ret)
1355 }
1356
1357 pub fn server_cosign(
1358 mut self,
1359 server_keypair: &Keypair,
1360 ) -> Result<ArkoorBuilder<state::ServerSigned>, ArkoorSigningError> {
1361 if server_keypair.public_key() != self.input.server_pubkey() {
1363 return Err(ArkoorSigningError::IncorrectKey {
1364 expected: self.input.server_pubkey(),
1365 got: server_keypair.public_key(),
1366 });
1367 }
1368
1369 let mut server_pub_nonces = Vec::with_capacity(self.outputs.len().saturating_add(1));
1370 let mut server_partial_sigs = Vec::with_capacity(self.outputs.len().saturating_add(1));
1371
1372 for idx in 0..self.nb_sigs() {
1373 let (server_pub_nonce, server_partial_sig) = musig::deterministic_partial_sign(
1374 &server_keypair,
1375 [self.input.user_pubkey()],
1376 &[&self.user_pub_nonces.as_ref().expect("state-invariant")[idx]],
1377 self.sighashes[idx].to_byte_array(),
1378 Some(self.taptweak_at(idx).to_byte_array()),
1379 );
1380
1381 server_pub_nonces.push(server_pub_nonce);
1382 server_partial_sigs.push(server_partial_sig);
1383 };
1384
1385 self.server_pub_nonces = Some(server_pub_nonces);
1386 self.server_partial_sigs = Some(server_partial_sigs);
1387 Ok(self.to_state::<state::ServerSigned>())
1388 }
1389}
1390
1391impl ArkoorBuilder<state::ServerSigned> {
1392 pub fn user_pub_nonces(&self) -> Vec<musig::PublicNonce> {
1393 self.user_pub_nonces.as_ref().expect("state invariant").clone()
1394 }
1395
1396 pub fn server_partial_signatures(&self) -> Vec<musig::PartialSignature> {
1397 self.server_partial_sigs.as_ref().expect("state invariant").clone()
1398 }
1399
1400 pub fn cosign_response(&self) -> ArkoorCosignResponse {
1401 ArkoorCosignResponse {
1402 server_pub_nonces: self.server_pub_nonces.as_ref()
1403 .expect("state invariant").clone(),
1404 server_partial_sigs: self.server_partial_sigs.as_ref()
1405 .expect("state invariant").clone(),
1406 }
1407 }
1408}
1409
1410impl ArkoorBuilder<state::UserGeneratedNonces> {
1411 pub fn user_pub_nonces(&self) -> &[PublicNonce] {
1412 self.user_pub_nonces.as_ref().expect("State invariant")
1413 }
1414
1415 pub fn cosign_request(&self) -> ArkoorCosignRequest<Vtxo<Full>> {
1416 ArkoorCosignRequest::new(
1417 self.user_pub_nonces().to_vec(),
1418 self.input.clone(),
1419 self.outputs.clone(),
1420 self.isolated_outputs.clone(),
1421 self.checkpoint_data.is_some(),
1422 self.user_keypair.as_ref().expect("State invariant"),
1423 )
1424 }
1425
1426 fn validate_server_cosign_response(
1427 &self,
1428 data: &ArkoorCosignResponse,
1429 ) -> Result<(), ArkoorSigningError> {
1430
1431 if data.server_pub_nonces.len() != self.nb_sigs() {
1433 return Err(ArkoorSigningError::InvalidNbServerNonces {
1434 expected: self.nb_sigs(),
1435 got: data.server_pub_nonces.len(),
1436 });
1437 }
1438
1439 if data.server_partial_sigs.len() != self.nb_sigs() {
1440 return Err(ArkoorSigningError::InvalidNbServerPartialSigs {
1441 expected: self.nb_sigs(),
1442 got: data.server_partial_sigs.len(),
1443 })
1444 }
1445
1446 for idx in 0..self.nb_sigs() {
1448 let is_valid_sig = scripts::verify_partial_sig(
1449 self.sighashes[idx],
1450 self.taptweak_at(idx),
1451 (self.input.server_pubkey(), &data.server_pub_nonces[idx]),
1452 (self.input.user_pubkey(), &self.user_pub_nonces()[idx]),
1453 &data.server_partial_sigs[idx]
1454 );
1455
1456 if !is_valid_sig {
1457 return Err(ArkoorSigningError::InvalidPartialSignature {
1458 index: idx,
1459 });
1460 }
1461 }
1462 Ok(())
1463 }
1464
1465 pub fn user_cosign(
1466 mut self,
1467 user_keypair: &Keypair,
1468 server_cosign_data: &ArkoorCosignResponse,
1469 ) -> Result<ArkoorBuilder<state::UserSigned>, ArkoorSigningError> {
1470 if user_keypair.public_key() != self.input.user_pubkey() {
1472 return Err(ArkoorSigningError::IncorrectKey {
1473 expected: self.input.user_pubkey(),
1474 got: user_keypair.public_key(),
1475 });
1476 }
1477
1478 self.validate_server_cosign_response(&server_cosign_data)?;
1480
1481 let mut sigs = Vec::with_capacity(self.nb_sigs());
1482
1483 let user_sec_nonces = self.user_sec_nonces.take().expect("state invariant");
1486
1487 for (idx, user_sec_nonce) in user_sec_nonces.into_iter().enumerate() {
1488 let user_pub_nonce = self.user_pub_nonces()[idx];
1489 let server_pub_nonce = server_cosign_data.server_pub_nonces[idx];
1490 let agg_nonce = musig::nonce_agg(&[&user_pub_nonce, &server_pub_nonce]);
1491
1492 let (_partial, maybe_sig) = musig::partial_sign(
1493 [self.user_pubkey(), self.server_pubkey()],
1494 agg_nonce,
1495 &user_keypair,
1496 user_sec_nonce,
1497 self.sighashes[idx].to_byte_array(),
1498 Some(self.taptweak_at(idx).to_byte_array()),
1499 Some(&[&server_cosign_data.server_partial_sigs[idx]])
1500 );
1501
1502 let sig = maybe_sig.expect("The full signature exists. The server did sign first");
1503 sigs.push(sig);
1504 }
1505
1506 self.full_signatures = Some(sigs);
1507
1508 Ok(self.to_state::<state::UserSigned>())
1509 }
1510}
1511
1512
1513impl<'a> ArkoorBuilder<state::UserSigned> {
1514 pub fn build_signed_vtxos(&self) -> Vec<Vtxo<Full>> {
1515 let sigs = self.full_signatures.as_ref().expect("state invariant");
1516 let mut ret = Vec::with_capacity(self.outputs.len().saturating_add(self.isolated_outputs.len()));
1517
1518 if self.checkpoint_data.is_some() {
1519 let checkpoint_sig = sigs[0];
1520
1521 for i in 0..self.outputs.len() {
1523 let arkoor_sig = sigs[i.saturating_add(1)];
1524 ret.push(self.build_vtxo_at(i, Some(checkpoint_sig), Some(arkoor_sig)));
1525 }
1526
1527 if self.unsigned_isolation_fanout_tx.is_some() {
1529 let m = self.outputs.len();
1530 let fanout_tx_sig = sigs[m.saturating_add(1)];
1531
1532 for i in 0..self.isolated_outputs.len() {
1533 ret.push(self.build_isolated_vtxo_at(
1534 i,
1535 Some(checkpoint_sig),
1536 Some(fanout_tx_sig),
1537 ));
1538 }
1539 }
1540 } else {
1541 let arkoor_sig = sigs[0];
1543
1544 for i in 0..self.outputs.len() {
1546 ret.push(self.build_vtxo_at(i, None, Some(arkoor_sig)));
1547 }
1548
1549 if self.unsigned_isolation_fanout_tx.is_some() {
1551 let fanout_tx_sig = sigs[1];
1552
1553 for i in 0..self.isolated_outputs.len() {
1554 ret.push(self.build_isolated_vtxo_at(
1555 i,
1556 Some(arkoor_sig), Some(fanout_tx_sig),
1558 ));
1559 }
1560 }
1561 }
1562
1563 ret
1564 }
1565
1566 pub fn signed_virtual_transactions(&self) -> Vec<Transaction> {
1571 let sigs = self.full_signatures.as_ref().expect("state invariant");
1572 let mut ret = Vec::new();
1573 let mut sig_idx = 0usize;
1574 if let Some((tx, _)) = &self.checkpoint_data {
1575 let mut tx = tx.clone();
1576 tx.input[0].witness.push(&sigs[sig_idx][..]);
1577 ret.push(tx);
1578 sig_idx = sig_idx.saturating_add(1);
1579 }
1580 for tx in &self.unsigned_arkoor_txs {
1581 let mut tx = tx.clone();
1582 tx.input[0].witness.push(&sigs[sig_idx][..]);
1583 ret.push(tx);
1584 sig_idx = sig_idx.saturating_add(1);
1585 }
1586 if let Some(tx) = &self.unsigned_isolation_fanout_tx {
1587 let mut tx = tx.clone();
1588 tx.input[0].witness.push(&sigs[sig_idx][..]);
1589 ret.push(tx);
1590 }
1591 ret
1592 }
1593
1594 pub fn build_signed_internal_vtxos(&self) -> Vec<(ServerVtxo<Full>, Txid)> {
1597 let sigs = self.full_signatures.as_ref().expect("state invariant");
1598 let intermediate_sig = if self.checkpoint_data.is_some() || !self.isolated_outputs.is_empty() {
1599 Some(sigs[0])
1600 } else {
1601 None
1602 };
1603 self.build_internal_vtxos(intermediate_sig)
1604 }
1605}
1606
1607fn arkoor_sighash(prevout: &TxOut, arkoor_tx: &Transaction) -> TapSighash {
1608 let mut shc = SighashCache::new(arkoor_tx);
1609
1610 shc.taproot_key_spend_signature_hash(
1611 0, &sighash::Prevouts::All(&[prevout]), TapSighashType::Default,
1612 ).expect("sighash error")
1613}
1614
1615#[cfg(test)]
1616mod test {
1617 use super::*;
1618
1619 use std::collections::HashSet;
1620
1621 use bitcoin::Amount;
1622 use bitcoin::secp256k1::Keypair;
1623 use bitcoin::secp256k1::rand;
1624
1625 use crate::SECP;
1626 use crate::test_util::dummy::DummyTestVtxoSpec;
1627 use crate::vtxo::VtxoId;
1628
1629 fn verify_signed_internal_vtxos(
1631 builder: &ArkoorBuilder<state::UserSigned>,
1632 funding_tx: &Transaction,
1633 ) {
1634 let signed = builder.build_signed_internal_vtxos();
1635 let unsigned = builder.build_unsigned_internal_vtxos();
1636 assert_eq!(signed.len(), unsigned.len());
1637
1638 for (vtxo, _spending_txid) in &signed {
1639 vtxo.validate(funding_tx).expect("signed internal vtxo must be valid");
1640 }
1641 }
1642
1643 fn verify_builder<S: state::BuilderState>(
1645 builder: &ArkoorBuilder<S>,
1646 input: &Vtxo<Full>,
1647 outputs: &[ArkoorDestination],
1648 isolated_outputs: &[ArkoorDestination],
1649 ) {
1650 let has_isolation = !isolated_outputs.is_empty();
1651
1652 let spend_info = builder.spend_info();
1653 let spend_vtxo_ids: HashSet<VtxoId> = spend_info.iter().map(|(id, _)| *id).collect();
1654
1655 assert_eq!(spend_info[0].0, input.id());
1657
1658 assert_eq!(spend_vtxo_ids.len(), spend_info.len());
1660
1661 let internal_vtxos = builder.build_unsigned_internal_vtxos();
1663 let internal_vtxo_ids = internal_vtxos.iter().map(|(v, _)| v.id()).collect::<HashSet<_>>();
1664 for (internal_vtxo, _spending_txid) in &internal_vtxos {
1665 assert!(spend_vtxo_ids.contains(&internal_vtxo.id()));
1666 assert!(matches!(internal_vtxo.policy(), ServerVtxoPolicy::Checkpoint(_)));
1667 }
1668
1669 for (vtxo_id, _) in &spend_info[1..] {
1671 assert!(internal_vtxo_ids.contains(vtxo_id));
1672 }
1673
1674 if has_isolation {
1676 let (isolation_vtxo, _) = internal_vtxos.last().unwrap();
1677 let expected_isolation_amount: Amount = isolated_outputs.iter()
1678 .map(|o| o.total_amount)
1679 .sum();
1680 assert_eq!(isolation_vtxo.amount(), expected_isolation_amount);
1681 }
1682
1683 let final_vtxos = builder.build_unsigned_vtxos().collect::<Vec<_>>();
1685 for final_vtxo in &final_vtxos {
1686 assert!(!spend_vtxo_ids.contains(&final_vtxo.id()));
1687 }
1688
1689 let all_destinations = outputs.iter()
1691 .chain(isolated_outputs.iter())
1692 .collect::<Vec<&_>>();
1693 for (vtxo, dest) in final_vtxos.iter().zip(all_destinations.iter()) {
1694 assert_eq!(vtxo.amount(), dest.total_amount);
1695 assert_eq!(vtxo.policy, dest.policy);
1696 }
1697
1698 let total_output_amount: Amount = final_vtxos.iter().map(|v| v.amount()).sum();
1700 assert_eq!(total_output_amount, input.amount());
1701 }
1702
1703 #[test]
1704 fn build_checkpointed_arkoor() {
1705 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1706 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1707 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1708
1709 println!("Alice keypair: {}", alice_keypair.public_key());
1710 println!("Bob keypair: {}", bob_keypair.public_key());
1711 println!("Server keypair: {}", server_keypair.public_key());
1712 println!("-----------------------------------------------");
1713
1714 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1715 amount: Amount::from_sat(100_330),
1716 fee: Amount::from_sat(330),
1717 expiry_height: 1000,
1718 exit_delta : 128,
1719 user_keypair: alice_keypair.clone(),
1720 server_keypair: server_keypair.clone()
1721 }.build();
1722
1723 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1725
1726 let dest = vec![
1727 ArkoorDestination {
1728 total_amount: Amount::from_sat(96_000),
1729 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1730 },
1731 ArkoorDestination {
1732 total_amount: Amount::from_sat(4_000),
1733 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1734 }
1735 ];
1736
1737 let user_builder = ArkoorBuilder::new_with_checkpoint(
1738 alice_vtxo.clone(),
1739 dest.clone(),
1740 vec![], ).expect("Valid arkoor request");
1742
1743 verify_builder(&user_builder, &alice_vtxo, &dest, &[]);
1744
1745 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1746 let cosign_request = user_builder.cosign_request();
1747
1748 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1750 .expect("Invalid cosign request")
1751 .server_cosign(&server_keypair)
1752 .expect("Incorrect key");
1753
1754 let cosign_data = server_builder.cosign_response();
1755
1756 let signed_builder = user_builder
1758 .user_cosign(&alice_keypair, &cosign_data)
1759 .expect("Valid cosign data and correct key");
1760 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
1761 let vtxos = signed_builder.build_signed_vtxos();
1762
1763 for vtxo in vtxos.into_iter() {
1764 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1766
1767 let mut prev_tx = funding_tx.clone();
1769 for tx in vtxo.transactions().map(|item| item.tx) {
1770 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1771 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1772 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1773 prev_tx = tx;
1774 }
1775 }
1776
1777 }
1778
1779 #[test]
1780 fn build_checkpointed_arkoor_with_dust_isolation() {
1781 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1784 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1785 let charlie_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1786 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1787
1788 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1789 amount: Amount::from_sat(100_330),
1790 fee: Amount::from_sat(330),
1791 expiry_height: 1000,
1792 exit_delta : 128,
1793 user_keypair: alice_keypair.clone(),
1794 server_keypair: server_keypair.clone()
1795 }.build();
1796
1797 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1799
1800 let outputs = vec![
1802 ArkoorDestination {
1803 total_amount: Amount::from_sat(99_600),
1804 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1805 },
1806 ];
1807
1808 let dust_outputs = vec![
1810 ArkoorDestination {
1811 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(charlie_keypair.public_key())
1813 },
1814 ArkoorDestination {
1815 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1817 }
1818 ];
1819
1820 let user_builder = ArkoorBuilder::new_with_checkpoint(
1821 alice_vtxo.clone(),
1822 outputs.clone(),
1823 dust_outputs.clone(),
1824 ).expect("Valid arkoor request with dust isolation");
1825
1826 verify_builder(&user_builder, &alice_vtxo, &outputs, &dust_outputs);
1827
1828 assert!(
1830 user_builder.unsigned_isolation_fanout_tx.is_some(),
1831 "Dust isolation should be active",
1832 );
1833
1834 assert_eq!(user_builder.nb_sigs(), 3);
1836
1837 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1838 let cosign_request = user_builder.cosign_request();
1839
1840 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1842 .expect("Invalid cosign request")
1843 .server_cosign(&server_keypair)
1844 .expect("Incorrect key");
1845
1846 let cosign_data = server_builder.cosign_response();
1847
1848 let signed_builder = user_builder
1850 .user_cosign(&alice_keypair, &cosign_data)
1851 .expect("Valid cosign data and correct key");
1852 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
1853 let vtxos = signed_builder.build_signed_vtxos();
1854
1855 assert_eq!(vtxos.len(), 3);
1857
1858 for vtxo in vtxos.into_iter() {
1859 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1861
1862 let mut prev_tx = funding_tx.clone();
1864 for tx in vtxo.transactions().map(|item| item.tx) {
1865 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1866 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1867 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1868 prev_tx = tx;
1869 }
1870 }
1871 }
1872
1873 #[test]
1874 fn build_no_checkpoint_arkoor() {
1875 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1876 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1877 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1878
1879 println!("Alice keypair: {}", alice_keypair.public_key());
1880 println!("Bob keypair: {}", bob_keypair.public_key());
1881 println!("Server keypair: {}", server_keypair.public_key());
1882 println!("-----------------------------------------------");
1883
1884 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1885 amount: Amount::from_sat(100_330),
1886 fee: Amount::from_sat(330),
1887 expiry_height: 1000,
1888 exit_delta : 128,
1889 user_keypair: alice_keypair.clone(),
1890 server_keypair: server_keypair.clone()
1891 }.build();
1892
1893 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1895
1896 let dest = vec![
1897 ArkoorDestination {
1898 total_amount: Amount::from_sat(96_000),
1899 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1900 },
1901 ArkoorDestination {
1902 total_amount: Amount::from_sat(4_000),
1903 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1904 }
1905 ];
1906
1907 let user_builder = ArkoorBuilder::new_without_checkpoint(
1908 alice_vtxo.clone(),
1909 dest.clone(),
1910 vec![], ).expect("Valid arkoor request");
1912
1913 verify_builder(&user_builder, &alice_vtxo, &dest, &[]);
1914
1915 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1916 let cosign_request = user_builder.cosign_request();
1917
1918 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1920 .expect("Invalid cosign request")
1921 .server_cosign(&server_keypair)
1922 .expect("Incorrect key");
1923
1924 let cosign_data = server_builder.cosign_response();
1925
1926 let signed_builder = user_builder
1928 .user_cosign(&alice_keypair, &cosign_data)
1929 .expect("Valid cosign data and correct key");
1930 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
1931 let vtxos = signed_builder.build_signed_vtxos();
1932
1933 for vtxo in vtxos.into_iter() {
1934 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1936
1937 let mut prev_tx = funding_tx.clone();
1939 for tx in vtxo.transactions().map(|item| item.tx) {
1940 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1941 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1942 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1943 prev_tx = tx;
1944 }
1945 }
1946
1947 }
1948
1949 #[test]
1950 fn build_no_checkpoint_arkoor_with_dust_isolation() {
1951 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1954 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1955 let charlie_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1956 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1957
1958 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1959 amount: Amount::from_sat(100_330),
1960 fee: Amount::from_sat(330),
1961 expiry_height: 1000,
1962 exit_delta : 128,
1963 user_keypair: alice_keypair.clone(),
1964 server_keypair: server_keypair.clone()
1965 }.build();
1966
1967 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1969
1970 let outputs = vec![
1972 ArkoorDestination {
1973 total_amount: Amount::from_sat(99_600),
1974 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1975 },
1976 ];
1977
1978 let dust_outputs = vec![
1980 ArkoorDestination {
1981 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(charlie_keypair.public_key())
1983 },
1984 ArkoorDestination {
1985 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1987 }
1988 ];
1989
1990 let user_builder = ArkoorBuilder::new_without_checkpoint(
1991 alice_vtxo.clone(),
1992 outputs.clone(),
1993 dust_outputs.clone(),
1994 ).expect("Valid arkoor request with dust isolation");
1995
1996 verify_builder(&user_builder, &alice_vtxo, &outputs, &dust_outputs);
1997
1998 assert!(
2000 user_builder.unsigned_isolation_fanout_tx.is_some(),
2001 "Dust isolation should be active",
2002 );
2003
2004 assert_eq!(user_builder.nb_sigs(), 2);
2007
2008 let user_builder = user_builder.generate_user_nonces(alice_keypair);
2009 let cosign_request = user_builder.cosign_request();
2010
2011 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
2013 .expect("Invalid cosign request")
2014 .server_cosign(&server_keypair)
2015 .expect("Incorrect key");
2016
2017 let cosign_data = server_builder.cosign_response();
2018
2019 let signed_builder = user_builder
2021 .user_cosign(&alice_keypair, &cosign_data)
2022 .expect("Valid cosign data and correct key");
2023 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
2024 let vtxos = signed_builder.build_signed_vtxos();
2025
2026 assert_eq!(vtxos.len(), 3);
2028
2029 for vtxo in vtxos.into_iter() {
2030 vtxo.validate(&funding_tx).expect("Invalid VTXO");
2032
2033 let mut prev_tx = funding_tx.clone();
2035 for tx in vtxo.transactions().map(|item| item.tx) {
2036 let prev_outpoint: OutPoint = tx.input[0].previous_output;
2037 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
2038 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
2039 prev_tx = tx;
2040 }
2041 }
2042 }
2043
2044 #[test]
2045 fn build_checkpointed_arkoor_outputs_must_be_above_dust_if_mixed() {
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(1_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 ArkoorBuilder::new_with_checkpoint(
2064 alice_vtxo.clone(),
2065 vec![
2066 ArkoorDestination {
2067 total_amount: Amount::from_sat(100), policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2069 }; 10
2070 ],
2071 vec![],
2072 ).unwrap();
2073
2074 let res_empty = ArkoorBuilder::new_with_checkpoint(
2076 alice_vtxo.clone(),
2077 vec![],
2078 vec![
2079 ArkoorDestination {
2080 total_amount: Amount::from_sat(100), policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2082 }; 10
2083 ],
2084 );
2085 match res_empty {
2086 Err(ArkoorConstructionError::NoOutputs) => {},
2087 _ => panic!("Expected NoOutputs error for empty outputs"),
2088 }
2089
2090 ArkoorBuilder::new_with_checkpoint(
2092 alice_vtxo.clone(),
2093 vec![
2094 ArkoorDestination {
2095 total_amount: Amount::from_sat(330), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2097 }; 2
2098 ],
2099 vec![
2100 ArkoorDestination {
2101 total_amount: Amount::from_sat(170),
2102 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2103 }; 2
2104 ],
2105 ).unwrap();
2106
2107 let res_mixed_small = ArkoorBuilder::new_with_checkpoint(
2109 alice_vtxo.clone(),
2110 vec![
2111 ArkoorDestination {
2112 total_amount: Amount::from_sat(500),
2113 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2114 },
2115 ArkoorDestination {
2116 total_amount: Amount::from_sat(300),
2117 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2118 }
2119 ],
2120 vec![
2121 ArkoorDestination {
2122 total_amount: Amount::from_sat(100),
2123 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2124 }; 2 ],
2126 );
2127 match res_mixed_small {
2128 Err(ArkoorConstructionError::Dust) => {},
2129 _ => panic!("Expected Dust error for isolation sum < 330"),
2130 }
2131 }
2132
2133 #[test]
2134 fn build_checkpointed_arkoor_dust_sum_too_small() {
2135 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2137 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2138 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2139
2140 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2141 amount: Amount::from_sat(100_330),
2142 fee: Amount::from_sat(330),
2143 expiry_height: 1000,
2144 exit_delta : 128,
2145 user_keypair: alice_keypair.clone(),
2146 server_keypair: server_keypair.clone()
2147 }.build();
2148
2149 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
2150
2151 let outputs = vec![
2153 ArkoorDestination {
2154 total_amount: Amount::from_sat(99_900),
2155 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2156 },
2157 ];
2158
2159 let dust_outputs = vec![
2161 ArkoorDestination {
2162 total_amount: Amount::from_sat(50),
2163 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2164 },
2165 ArkoorDestination {
2166 total_amount: Amount::from_sat(50),
2167 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2168 }
2169 ];
2170
2171 let result = ArkoorBuilder::new_with_checkpoint(
2173 alice_vtxo.clone(),
2174 outputs.clone(),
2175 dust_outputs.clone(),
2176 );
2177 match result {
2178 Err(ArkoorConstructionError::Dust) => {},
2179 _ => panic!("Expected Dust error for isolation sum < 330"),
2180 }
2181 }
2182
2183 #[test]
2184 fn spend_dust_vtxo() {
2185 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2187 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2188 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2189
2190 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2192 amount: Amount::from_sat(200),
2193 fee: Amount::ZERO,
2194 expiry_height: 1000,
2195 exit_delta: 128,
2196 user_keypair: alice_keypair.clone(),
2197 server_keypair: server_keypair.clone()
2198 }.build();
2199
2200 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
2201
2202 let dust_outputs = vec![
2205 ArkoorDestination {
2206 total_amount: Amount::from_sat(100),
2207 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2208 },
2209 ArkoorDestination {
2210 total_amount: Amount::from_sat(100),
2211 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2212 }
2213 ];
2214
2215 let user_builder = ArkoorBuilder::new_with_checkpoint(
2216 alice_vtxo.clone(),
2217 dust_outputs,
2218 vec![],
2219 ).expect("Valid arkoor request for all-dust case");
2220
2221 assert!(
2223 user_builder.unsigned_isolation_fanout_tx.is_none(),
2224 "Dust isolation should NOT be active",
2225 );
2226
2227 assert_eq!(user_builder.outputs.len(), 2);
2229
2230 assert_eq!(user_builder.nb_sigs(), 3);
2232
2233 let user_builder = user_builder.generate_user_nonces(alice_keypair);
2235 let cosign_request = user_builder.cosign_request();
2236
2237 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
2239 .expect("Invalid cosign request")
2240 .server_cosign(&server_keypair)
2241 .expect("Incorrect key");
2242
2243 let cosign_data = server_builder.cosign_response();
2244
2245 let signed_builder = user_builder
2247 .user_cosign(&alice_keypair, &cosign_data)
2248 .expect("Valid cosign data and correct key");
2249 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
2250 let vtxos = signed_builder.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(100));
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 spend_nondust_vtxo_to_dust() {
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 {
2283 amount: Amount::from_sat(500),
2284 fee: Amount::ZERO,
2285 expiry_height: 1000,
2286 exit_delta: 128,
2287 user_keypair: alice_keypair.clone(),
2288 server_keypair: server_keypair.clone()
2289 }.build();
2290
2291 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
2292
2293 let dust_outputs = vec![
2296 ArkoorDestination {
2297 total_amount: Amount::from_sat(250),
2298 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2299 },
2300 ArkoorDestination {
2301 total_amount: Amount::from_sat(250),
2302 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2303 }
2304 ];
2305
2306 let user_builder = ArkoorBuilder::new_with_checkpoint(
2307 alice_vtxo.clone(),
2308 dust_outputs,
2309 vec![],
2310 ).expect("Valid arkoor request for non-dust to dust case");
2311
2312 assert!(
2314 user_builder.unsigned_isolation_fanout_tx.is_none(),
2315 "Dust isolation should NOT be active",
2316 );
2317
2318 assert_eq!(user_builder.outputs.len(), 2);
2320
2321 assert_eq!(user_builder.nb_sigs(), 3);
2323
2324 let user_builder = user_builder.generate_user_nonces(alice_keypair);
2326 let cosign_request = user_builder.cosign_request();
2327
2328 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
2330 .expect("Invalid cosign request")
2331 .server_cosign(&server_keypair)
2332 .expect("Incorrect key");
2333
2334 let cosign_data = server_builder.cosign_response();
2335
2336 let signed_builder = user_builder
2338 .user_cosign(&alice_keypair, &cosign_data)
2339 .expect("Valid cosign data and correct key");
2340 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
2341 let vtxos = signed_builder.build_signed_vtxos();
2342
2343 assert_eq!(vtxos.len(), 2);
2345
2346 for vtxo in vtxos.into_iter() {
2347 vtxo.validate(&funding_tx).expect("Invalid VTXO");
2349
2350 assert_eq!(vtxo.amount(), Amount::from_sat(250));
2352
2353 let mut prev_tx = funding_tx.clone();
2355 for tx in vtxo.transactions().map(|item| item.tx) {
2356 let prev_outpoint: OutPoint = tx.input[0].previous_output;
2357 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
2358 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
2359 prev_tx = tx;
2360 }
2361 }
2362 }
2363
2364 #[test]
2365 fn isolate_dust_all_nondust() {
2366 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2369 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2370 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2371
2372 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2373 amount: Amount::from_sat(1000),
2374 fee: Amount::ZERO,
2375 expiry_height: 1000,
2376 exit_delta: 128,
2377 user_keypair: alice_keypair.clone(),
2378 server_keypair: server_keypair.clone()
2379 }.build();
2380
2381 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2382
2383 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2384 alice_vtxo,
2385 vec![
2386 ArkoorDestination {
2387 total_amount: Amount::from_sat(500),
2388 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2389 },
2390 ArkoorDestination {
2391 total_amount: Amount::from_sat(500),
2392 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2393 }
2394 ],
2395 ).unwrap();
2396
2397 assert!(builder.unsigned_isolation_fanout_tx.is_none());
2399
2400 assert_eq!(builder.outputs.len(), 2);
2402 assert_eq!(builder.isolated_outputs.len(), 0);
2403 }
2404
2405 #[test]
2406 fn isolate_dust_all_dust() {
2407 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2410 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2411 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2412
2413 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2414 amount: Amount::from_sat(400),
2415 fee: Amount::ZERO,
2416 expiry_height: 1000,
2417 exit_delta: 128,
2418 user_keypair: alice_keypair.clone(),
2419 server_keypair: server_keypair.clone()
2420 }.build();
2421
2422 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2423
2424 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2425 alice_vtxo,
2426 vec![
2427 ArkoorDestination {
2428 total_amount: Amount::from_sat(200),
2429 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2430 },
2431 ArkoorDestination {
2432 total_amount: Amount::from_sat(200),
2433 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2434 }
2435 ],
2436 ).unwrap();
2437
2438 assert!(builder.unsigned_isolation_fanout_tx.is_none());
2440
2441 assert_eq!(builder.outputs.len(), 2);
2443 assert_eq!(builder.isolated_outputs.len(), 0);
2444 }
2445
2446 #[test]
2447 fn isolate_dust_sufficient_dust() {
2448 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2451 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2452 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2453
2454 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2455 amount: Amount::from_sat(1000),
2456 fee: Amount::ZERO,
2457 expiry_height: 1000,
2458 exit_delta: 128,
2459 user_keypair: alice_keypair.clone(),
2460 server_keypair: server_keypair.clone()
2461 }.build();
2462
2463 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2464
2465 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2467 alice_vtxo,
2468 vec![
2469 ArkoorDestination {
2470 total_amount: Amount::from_sat(600),
2471 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2472 },
2473 ArkoorDestination {
2474 total_amount: Amount::from_sat(200),
2475 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2476 },
2477 ArkoorDestination {
2478 total_amount: Amount::from_sat(200),
2479 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2480 }
2481 ],
2482 ).unwrap();
2483
2484 assert!(builder.unsigned_isolation_fanout_tx.is_some());
2486
2487 assert_eq!(builder.outputs.len(), 1);
2489 assert_eq!(builder.isolated_outputs.len(), 2);
2490 }
2491
2492 #[test]
2493 fn isolate_dust_split_successful() {
2494 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2498 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2499 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2500
2501 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2502 amount: Amount::from_sat(1000),
2503 fee: Amount::ZERO,
2504 expiry_height: 1000,
2505 exit_delta: 128,
2506 user_keypair: alice_keypair.clone(),
2507 server_keypair: server_keypair.clone()
2508 }.build();
2509
2510 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2511
2512 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2513 alice_vtxo,
2514 vec![
2515 ArkoorDestination {
2516 total_amount: Amount::from_sat(800),
2517 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2518 },
2519 ArkoorDestination {
2520 total_amount: Amount::from_sat(100),
2521 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2522 },
2523 ArkoorDestination {
2524 total_amount: Amount::from_sat(100),
2525 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2526 }
2527 ],
2528 ).unwrap();
2529
2530 assert!(builder.unsigned_isolation_fanout_tx.is_some());
2532
2533 assert_eq!(builder.outputs.len(), 1);
2535 assert_eq!(builder.isolated_outputs.len(), 3);
2536
2537 assert_eq!(builder.outputs[0].total_amount, Amount::from_sat(670));
2539 let isolated_sum: Amount = builder.isolated_outputs.iter().map(|o| o.total_amount).sum();
2540 assert_eq!(isolated_sum, P2TR_DUST);
2541 }
2542
2543 #[test]
2544 fn isolate_dust_split_impossible() {
2545 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2550 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2551 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2552
2553 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2554 amount: Amount::from_sat(600),
2555 fee: Amount::ZERO,
2556 expiry_height: 1000,
2557 exit_delta: 128,
2558 user_keypair: alice_keypair.clone(),
2559 server_keypair: server_keypair.clone()
2560 }.build();
2561
2562 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2563
2564 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2565 alice_vtxo,
2566 vec![
2567 ArkoorDestination {
2568 total_amount: Amount::from_sat(400),
2569 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2570 },
2571 ArkoorDestination {
2572 total_amount: Amount::from_sat(100),
2573 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2574 },
2575 ArkoorDestination {
2576 total_amount: Amount::from_sat(100),
2577 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2578 }
2579 ],
2580 ).unwrap();
2581
2582 assert!(builder.unsigned_isolation_fanout_tx.is_none());
2584
2585 assert_eq!(builder.outputs.len(), 3);
2587 assert_eq!(builder.isolated_outputs.len(), 0);
2588 }
2589
2590 #[test]
2591 fn isolate_dust_exactly_boundary() {
2592 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2596 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2597 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2598
2599 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2600 amount: Amount::from_sat(1000),
2601 fee: Amount::ZERO,
2602 expiry_height: 1000,
2603 exit_delta: 128,
2604 user_keypair: alice_keypair.clone(),
2605 server_keypair: server_keypair.clone()
2606 }.build();
2607
2608 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2609
2610 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2611 alice_vtxo,
2612 vec![
2613 ArkoorDestination {
2614 total_amount: Amount::from_sat(660),
2615 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2616 },
2617 ArkoorDestination {
2618 total_amount: Amount::from_sat(170),
2619 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2620 },
2621 ArkoorDestination {
2622 total_amount: Amount::from_sat(170),
2623 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2624 }
2625 ],
2626 ).unwrap();
2627
2628 assert!(builder.unsigned_isolation_fanout_tx.is_some());
2630
2631 assert_eq!(builder.outputs.len(), 1);
2633 assert_eq!(builder.isolated_outputs.len(), 2);
2634
2635 assert_eq!(builder.outputs[0].total_amount, Amount::from_sat(660));
2637 assert_eq!(builder.isolated_outputs[0].total_amount, Amount::from_sat(170));
2638 assert_eq!(builder.isolated_outputs[1].total_amount, Amount::from_sat(170));
2639 }
2640
2641 #[test]
2642 fn validate_amounts_output_sum_overflow_rejected() {
2643 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2648 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2649 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2650
2651 let (_funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2652 amount: Amount::from_sat(10_330),
2653 fee: Amount::from_sat(330),
2654 expiry_height: 1000,
2655 exit_delta: 128,
2656 user_keypair: alice_keypair,
2657 server_keypair,
2658 }.build();
2659
2660 let outputs = vec![
2661 ArkoorDestination {
2662 total_amount: Amount::from_sat(u64::MAX),
2663 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key()),
2664 },
2665 ArkoorDestination {
2666 total_amount: Amount::from_sat(u64::MAX),
2667 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key()),
2668 },
2669 ];
2670
2671 let result = ArkoorBuilder::new_with_checkpoint(alice_vtxo, outputs, vec![]);
2672 assert_eq!(result.err(), Some(ArkoorConstructionError::Overflow));
2673 }
2674}