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