1pub mod package;
105
106use std::marker::PhantomData;
107
108use bitcoin::hashes::Hash;
109use bitcoin::sighash::{self, SighashCache};
110use bitcoin::amount::CheckedSum;
111use bitcoin::{
112 Amount, OutPoint, ScriptBuf, Sequence, TapSighash, TapSighashType, Transaction, TxIn, TxOut, Txid, Witness
113};
114use bitcoin::taproot::TapTweakHash;
115use bitcoin::secp256k1::{schnorr, Keypair, PublicKey};
116use bitcoin_ext::{fee, P2TR_DUST, TxOutExt};
117use secp256k1_musig::musig::PublicNonce;
118
119use crate::{musig, scripts, Vtxo, VtxoId, ServerVtxo};
120use crate::attestations::ArkoorCosignAttestation;
121use crate::vtxo::{Full, ServerVtxoPolicy, VtxoPolicy, VtxoRef};
122use crate::vtxo::genesis::{GenesisItem, GenesisTransition};
123
124pub use package::ArkoorPackageBuilder;
125
126
127#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
128pub enum ArkoorConstructionError {
129 #[error("Input amount of {input} does not match output amount of {output}")]
130 Unbalanced {
131 input: Amount,
132 output: Amount,
133 },
134 #[error("An output is below the dust threshold")]
135 Dust,
136 #[error("Dust isolation is used but not needed")]
137 IsolationNotNeeded,
138 #[error("At least one output is required")]
139 NoOutputs,
140 #[error("An output has zero value")]
141 ZeroValueOutput,
142 #[error("Too many outputs provided")]
143 TooManyOutputs,
144 #[error("Too many inputs provided")]
145 TooManyInputs,
146 #[error("Total amount overflowed while allocating outputs to inputs")]
147 Overflow,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
151pub enum ArkoorSigningError {
152 #[error("Invalid attestation")]
153 InvalidAttestation(AttestationError),
154 #[error("An error occurred while building arkoor: {0}")]
155 ArkoorConstructionError(ArkoorConstructionError),
156 #[error("Wrong number of user nonces provided. Expected {expected}, got {got}")]
157 InvalidNbUserNonces {
158 expected: usize,
159 got: usize,
160 },
161 #[error("Wrong number of server nonces provided. Expected {expected}, got {got}")]
162 InvalidNbServerNonces {
163 expected: usize,
164 got: usize,
165 },
166 #[error("Incorrect signing key provided. Expected {expected}, got {got}")]
167 IncorrectKey {
168 expected: PublicKey,
169 got: PublicKey,
170 },
171 #[error("Wrong number of server partial sigs. Expected {expected}, got {got}")]
172 InvalidNbServerPartialSigs {
173 expected: usize,
174 got: usize
175 },
176 #[error("Invalid partial signature at index {index}")]
177 InvalidPartialSignature {
178 index: usize,
179 },
180 #[error("Wrong number of packages. Expected {expected}, got {got}")]
181 InvalidNbPackages {
182 expected: usize,
183 got: usize,
184 },
185 #[error("Wrong number of keypairs. Expected {expected}, got {got}")]
186 InvalidNbKeypairs {
187 expected: usize,
188 got: usize,
189 },
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
197pub struct ArkoorDestination {
198 pub total_amount: Amount,
199 #[serde(with = "crate::encode::serde")]
200 pub policy: VtxoPolicy,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct ArkoorCosignResponse {
205 pub server_pub_nonces: Vec<musig::PublicNonce>,
206 pub server_partial_sigs: Vec<musig::PartialSignature>,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct ArkoorCosignRequest<V> {
211 pub user_pub_nonces: Vec<musig::PublicNonce>,
212 pub input: V,
213 pub outputs: Vec<ArkoorDestination>,
214 pub isolated_outputs: Vec<ArkoorDestination>,
215 pub use_checkpoint: bool,
216 pub attestation: ArkoorCosignAttestation,
217}
218
219impl<V> ArkoorCosignRequest<V> {
220 pub fn new_with_attestation(
221 user_pub_nonces: Vec<musig::PublicNonce>,
222 input: V,
223 outputs: Vec<ArkoorDestination>,
224 isolated_outputs: Vec<ArkoorDestination>,
225 use_checkpoint: bool,
226 attestation: ArkoorCosignAttestation,
227 ) -> Self {
228 Self {
229 user_pub_nonces,
230 input,
231 outputs,
232 isolated_outputs,
233 use_checkpoint,
234 attestation,
235 }
236 }
237
238 pub fn all_outputs(&self) -> impl Iterator<Item = &ArkoorDestination> + Clone {
239 self.outputs.iter().chain(&self.isolated_outputs)
240 }
241}
242
243impl<V: VtxoRef> ArkoorCosignRequest<V> {
244 pub fn new(
245 user_pub_nonces: Vec<musig::PublicNonce>,
246 input: V,
247 outputs: Vec<ArkoorDestination>,
248 isolated_outputs: Vec<ArkoorDestination>,
249 use_checkpoint: bool,
250 keypair: &Keypair,
251 ) -> Self {
252 let all_outputs = &outputs.iter().chain(&isolated_outputs).collect::<Vec<_>>();
253 let attestation = ArkoorCosignAttestation::new(input.vtxo_id(), all_outputs, keypair);
254
255 Self::new_with_attestation(
256 user_pub_nonces,
257 input,
258 outputs,
259 isolated_outputs,
260 use_checkpoint,
261 attestation,
262 )
263 }
264}
265
266impl ArkoorCosignRequest<VtxoId> {
267 pub fn with_vtxo(self, vtxo: Vtxo<Full>) -> Result<ArkoorCosignRequest<Vtxo<Full>>, &'static str> {
268 if self.input != vtxo.id() {
269 return Err("Input vtxo id does not match the provided vtxo id")
270 }
271
272 Ok(ArkoorCosignRequest::new_with_attestation(
273 self.user_pub_nonces,
274 vtxo,
275 self.outputs,
276 self.isolated_outputs,
277 self.use_checkpoint,
278 self.attestation,
279 ))
280 }
281}
282
283#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Hash)]
284#[error("invalid attestation")]
285pub struct AttestationError;
286
287impl ArkoorCosignRequest<Vtxo> {
288 pub fn verify_attestation(&self) -> Result<(), AttestationError> {
289 let outputs = self.all_outputs().collect::<Vec<_>>();
290 self.attestation.verify(&self.input, &outputs)
291 .map_err(|_| AttestationError)
292 }
293}
294
295pub mod state {
296 mod sealed {
304 pub trait Sealed {}
305 impl Sealed for super::Initial {}
306 impl Sealed for super::UserGeneratedNonces {}
307 impl Sealed for super::UserSigned {}
308 impl Sealed for super::ServerCanCosign {}
309 impl Sealed for super::ServerSigned {}
310 }
311
312 pub trait BuilderState: sealed::Sealed {}
313
314 pub struct Initial;
316 impl BuilderState for Initial {}
317
318 pub struct UserGeneratedNonces;
320 impl BuilderState for UserGeneratedNonces {}
321
322 pub struct UserSigned;
324 impl BuilderState for UserSigned {}
325
326 pub struct ServerCanCosign;
328 impl BuilderState for ServerCanCosign {}
329
330
331 pub struct ServerSigned;
333 impl BuilderState for ServerSigned {}
334}
335
336pub struct ArkoorBuilder<S: state::BuilderState> {
337 input: Vtxo<Full>,
340 outputs: Vec<ArkoorDestination>,
347 isolated_outputs: Vec<ArkoorDestination>,
352
353 checkpoint_data: Option<(Transaction, Txid)>,
358 unsigned_arkoor_txs: Vec<Transaction>,
360 unsigned_isolation_fanout_tx: Option<Transaction>,
363 sighashes: Vec<TapSighash>,
365 input_tweak: TapTweakHash,
367 checkpoint_policy_tweak: TapTweakHash,
370 new_vtxo_ids: Vec<VtxoId>,
372
373 user_keypair: Option<Keypair>,
378 user_pub_nonces: Option<Vec<musig::PublicNonce>>,
380 user_sec_nonces: Option<Vec<musig::SecretNonce>>,
382 server_pub_nonces: Option<Vec<musig::PublicNonce>>,
384 server_partial_sigs: Option<Vec<musig::PartialSignature>>,
386 full_signatures: Option<Vec<schnorr::Signature>>,
388
389 _state: PhantomData<S>,
390}
391
392impl<S: state::BuilderState> ArkoorBuilder<S> {
393 pub fn input(&self) -> &Vtxo<Full> {
395 &self.input
396 }
397
398 pub fn normal_outputs(&self) -> &[ArkoorDestination] {
400 &self.outputs
401 }
402
403 pub fn isolated_outputs(&self) -> &[ArkoorDestination] {
405 &self.isolated_outputs
406 }
407
408 pub fn all_outputs(
410 &self,
411 ) -> impl Iterator<Item = &ArkoorDestination> + Clone {
412 self.outputs.iter().chain(&self.isolated_outputs)
413 }
414
415 fn build_checkpoint_vtxo_at(
416 &self,
417 output_idx: usize,
418 checkpoint_sig: Option<schnorr::Signature>
419 ) -> ServerVtxo<Full> {
420 let output = &self.outputs[output_idx];
421 let (checkpoint_tx, checkpoint_txid) = self.checkpoint_data.as_ref()
422 .expect("called checkpoint_vtxo_at in context without checkpoints");
423
424 Vtxo {
425 amount: output.total_amount,
426 policy: ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey()),
427 expiry_height: self.input.expiry_height,
428 server_pubkey: self.input.server_pubkey,
429 exit_delta: self.input.exit_delta,
430 point: OutPoint::new(*checkpoint_txid, u32::try_from(output_idx).expect("output index fits in u32")),
431 anchor_point: self.input.anchor_point,
432 genesis: Full {
433 items: self.input.genesis.items.clone().into_iter().chain([
434 GenesisItem {
435 transition: GenesisTransition::new_arkoor(
436 vec![self.input.user_pubkey()],
437 self.input.policy().taproot(
438 self.input.server_pubkey,
439 self.input.exit_delta,
440 self.input.expiry_height,
441 ).tap_tweak(),
442 checkpoint_sig,
443 ),
444 output_idx: u8::try_from(output_idx).expect("arkoor output index fits in u8"),
445 other_outputs: checkpoint_tx.output
446 .iter().enumerate()
447 .filter_map(|(i, txout)| {
448 if i == output_idx || txout.is_p2a_fee_anchor() {
449 None
450 } else {
451 Some(txout.clone())
452 }
453 })
454 .collect(),
455 fee_amount: Amount::ZERO,
456 },
457 ]).collect(),
458 },
459 }
460 }
461
462 fn build_vtxo_at(
463 &self,
464 output_idx: usize,
465 checkpoint_sig: Option<schnorr::Signature>,
466 arkoor_sig: Option<schnorr::Signature>,
467 ) -> Vtxo<Full> {
468 let output = &self.outputs[output_idx];
469
470 if let Some((checkpoint_tx, _txid)) = &self.checkpoint_data {
471 let checkpoint_policy = ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey());
473
474 Vtxo {
475 amount: output.total_amount,
476 policy: output.policy.clone(),
477 expiry_height: self.input.expiry_height,
478 server_pubkey: self.input.server_pubkey,
479 exit_delta: self.input.exit_delta,
480 point: self.new_vtxo_ids[output_idx].to_point(),
481 anchor_point: self.input.anchor_point,
482 genesis: Full {
483 items: self.input.genesis.items.iter().cloned().chain([
484 GenesisItem {
485 transition: GenesisTransition::new_arkoor(
486 vec![self.input.user_pubkey()],
487 self.input.policy.taproot(
488 self.input.server_pubkey,
489 self.input.exit_delta,
490 self.input.expiry_height,
491 ).tap_tweak(),
492 checkpoint_sig,
493 ),
494 output_idx: u8::try_from(output_idx).expect("arkoor output index fits in u8"),
495 other_outputs: checkpoint_tx.output
496 .iter().enumerate()
497 .filter_map(|(i, txout)| {
498 if i == output_idx || txout.is_p2a_fee_anchor() {
499 None
500 } else {
501 Some(txout.clone())
502 }
503 })
504 .collect(),
505 fee_amount: Amount::ZERO,
506 },
507 GenesisItem {
508 transition: GenesisTransition::new_arkoor(
509 vec![self.input.user_pubkey()],
510 checkpoint_policy.taproot(
511 self.input.server_pubkey,
512 self.input.exit_delta,
513 self.input.expiry_height,
514 ).tap_tweak(),
515 arkoor_sig,
516 ),
517 output_idx: 0,
518 other_outputs: vec![],
519 fee_amount: Amount::ZERO,
520 }
521 ]).collect(),
522 },
523 }
524 } else {
525 let arkoor_tx = &self.unsigned_arkoor_txs[0];
527
528 Vtxo {
529 amount: output.total_amount,
530 policy: output.policy.clone(),
531 expiry_height: self.input.expiry_height,
532 server_pubkey: self.input.server_pubkey,
533 exit_delta: self.input.exit_delta,
534 point: OutPoint::new(arkoor_tx.compute_txid(), u32::try_from(output_idx).expect("output index fits in u32")),
535 anchor_point: self.input.anchor_point,
536 genesis: Full {
537 items: self.input.genesis.items.iter().cloned().chain([
538 GenesisItem {
539 transition: GenesisTransition::new_arkoor(
540 vec![self.input.user_pubkey()],
541 self.input.policy.taproot(
542 self.input.server_pubkey,
543 self.input.exit_delta,
544 self.input.expiry_height,
545 ).tap_tweak(),
546 arkoor_sig,
547 ),
548 output_idx: u8::try_from(output_idx).expect("arkoor output index fits in u8"),
549 other_outputs: arkoor_tx.output
550 .iter().enumerate()
551 .filter_map(|(idx, txout)| {
552 if idx == output_idx || txout.is_p2a_fee_anchor() {
553 None
554 } else {
555 Some(txout.clone())
556 }
557 })
558 .collect(),
559 fee_amount: Amount::ZERO,
560 }
561 ]).collect(),
562 },
563 }
564 }
565 }
566
567 fn build_isolated_vtxo_at(
575 &self,
576 isolated_idx: usize,
577 pre_fanout_tx_sig: Option<schnorr::Signature>,
578 isolation_fanout_tx_sig: Option<schnorr::Signature>,
579 ) -> Vtxo<Full> {
580 let output = &self.isolated_outputs[isolated_idx];
581 let checkpoint_policy = ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey());
582
583 let fanout_tx = self.unsigned_isolation_fanout_tx.as_ref()
584 .expect("construct_dust_vtxo_at called without dust isolation");
585
586 let dust_isolation_output_idx = self.outputs.len();
588
589 if let Some((checkpoint_tx, _txid)) = &self.checkpoint_data {
590 Vtxo {
592 amount: output.total_amount,
593 policy: output.policy.clone(),
594 expiry_height: self.input.expiry_height,
595 server_pubkey: self.input.server_pubkey,
596 exit_delta: self.input.exit_delta,
597 point: OutPoint::new(fanout_tx.compute_txid(), u32::try_from(isolated_idx).expect("output index fits in u32")),
598 anchor_point: self.input.anchor_point,
599 genesis: Full {
600 items: self.input.genesis.items.iter().cloned().chain([
601 GenesisItem {
603 transition: GenesisTransition::new_arkoor(
604 vec![self.input.user_pubkey()],
605 self.input.policy.taproot(
606 self.input.server_pubkey,
607 self.input.exit_delta,
608 self.input.expiry_height,
609 ).tap_tweak(),
610 pre_fanout_tx_sig,
611 ),
612 output_idx: u8::try_from(dust_isolation_output_idx).expect("arkoor output index fits in u8"),
613 other_outputs: checkpoint_tx.output
616 .iter().enumerate()
617 .filter_map(|(idx, txout)| {
618 let is_p2a = txout.is_p2a_fee_anchor();
619 if idx == dust_isolation_output_idx || is_p2a {
620 None
621 } else {
622 Some(txout.clone())
623 }
624 })
625 .collect(),
626 fee_amount: Amount::ZERO,
627 },
628 GenesisItem {
630 transition: GenesisTransition::new_arkoor(
631 vec![self.input.user_pubkey()],
632 checkpoint_policy.taproot(
633 self.input.server_pubkey,
634 self.input.exit_delta,
635 self.input.expiry_height,
636 ).tap_tweak(),
637 isolation_fanout_tx_sig,
638 ),
639 output_idx: u8::try_from(isolated_idx).expect("arkoor output index fits in u8"),
640 other_outputs: fanout_tx.output
643 .iter().enumerate()
644 .filter_map(|(idx, txout)| {
645 if idx == isolated_idx || txout.is_p2a_fee_anchor() {
646 None
647 } else {
648 Some(txout.clone())
649 }
650 })
651 .collect(),
652 fee_amount: Amount::ZERO,
653 },
654 ]).collect(),
655 },
656 }
657 } else {
658 let arkoor_tx = &self.unsigned_arkoor_txs[0];
660
661 Vtxo {
662 amount: output.total_amount,
663 policy: output.policy.clone(),
664 expiry_height: self.input.expiry_height,
665 server_pubkey: self.input.server_pubkey,
666 exit_delta: self.input.exit_delta,
667 point: OutPoint::new(fanout_tx.compute_txid(), u32::try_from(isolated_idx).expect("output index fits in u32")),
668 anchor_point: self.input.anchor_point,
669 genesis: Full {
670 items: self.input.genesis.items.iter().cloned().chain([
671 GenesisItem {
673 transition: GenesisTransition::new_arkoor(
674 vec![self.input.user_pubkey()],
675 self.input.policy.taproot(
676 self.input.server_pubkey,
677 self.input.exit_delta,
678 self.input.expiry_height,
679 ).tap_tweak(),
680 pre_fanout_tx_sig,
681 ),
682 output_idx: u8::try_from(dust_isolation_output_idx).expect("arkoor output index fits in u8"),
683 other_outputs: arkoor_tx.output
684 .iter().enumerate()
685 .filter_map(|(idx, txout)| {
686 if idx == dust_isolation_output_idx || txout.is_p2a_fee_anchor() {
687 None
688 } else {
689 Some(txout.clone())
690 }
691 })
692 .collect(),
693 fee_amount: Amount::ZERO,
694 },
695 GenesisItem {
697 transition: GenesisTransition::new_arkoor(
698 vec![self.input.user_pubkey()],
699 checkpoint_policy.taproot(
700 self.input.server_pubkey,
701 self.input.exit_delta,
702 self.input.expiry_height,
703 ).tap_tweak(),
704 isolation_fanout_tx_sig,
705 ),
706 output_idx: u8::try_from(isolated_idx).expect("arkoor output index fits in u8"),
707 other_outputs: fanout_tx.output
708 .iter().enumerate()
709 .filter_map(|(idx, txout)| {
710 if idx == isolated_idx || txout.is_p2a_fee_anchor() {
711 None
712 } else {
713 Some(txout.clone())
714 }
715 })
716 .collect(),
717 fee_amount: Amount::ZERO,
718 },
719 ]).collect(),
720 },
721 }
722 }
723 }
724
725 fn nb_sigs(&self) -> usize {
726 let base = if self.checkpoint_data.is_some() {
727 self.outputs.len().saturating_add(1) } else {
729 1 };
731
732 if self.unsigned_isolation_fanout_tx.is_some() {
733 base.saturating_add(1) } else {
735 base
736 }
737 }
738
739 pub fn build_unsigned_vtxos<'a>(&'a self) -> impl Iterator<Item = Vtxo<Full>> + 'a {
740 let regular = (0..self.outputs.len()).map(|i| self.build_vtxo_at(i, None, None));
741 let isolated = (0..self.isolated_outputs.len())
742 .map(|i| self.build_isolated_vtxo_at(i, None, None));
743 regular.chain(isolated)
744 }
745
746 fn build_internal_vtxos(
752 &self,
753 intermediate_sig: Option<schnorr::Signature>,
754 ) -> Vec<(ServerVtxo<Full>, Txid)> {
755 let mut ret = Vec::new();
756
757 if self.checkpoint_data.is_some() {
758 for idx in 0..self.outputs.len() {
759 let vtxo = self.build_checkpoint_vtxo_at(idx, intermediate_sig);
760 let spending_txid = self.unsigned_arkoor_txs[idx].compute_txid();
761 ret.push((vtxo, spending_txid));
762 }
763 }
764
765 if !self.isolated_outputs.is_empty() {
766 let output_idx = self.outputs.len();
767
768 let (int_tx, int_txid) = if let Some((tx, txid)) = &self.checkpoint_data {
769 (tx, *txid)
770 } else {
771 let arkoor_tx = &self.unsigned_arkoor_txs[0];
772 (arkoor_tx, arkoor_tx.compute_txid())
773 };
774
775 let vtxo = Vtxo {
776 amount: self.isolated_outputs.iter().map(|o| o.total_amount).sum(),
777 policy: ServerVtxoPolicy::new_checkpoint(self.input.user_pubkey()),
778 expiry_height: self.input.expiry_height,
779 server_pubkey: self.input.server_pubkey,
780 exit_delta: self.input.exit_delta,
781 point: OutPoint::new(int_txid, u32::try_from(output_idx).expect("output index fits in u32")),
782 anchor_point: self.input.anchor_point,
783 genesis: Full {
784 items: self.input.genesis.items.clone().into_iter().chain([
785 GenesisItem {
786 transition: GenesisTransition::new_arkoor(
787 vec![self.input.user_pubkey()],
788 self.input_tweak,
789 intermediate_sig,
790 ),
791 output_idx: u8::try_from(output_idx).expect("arkoor output index fits in u8"),
792 other_outputs: int_tx.output.iter().enumerate()
793 .filter_map(|(i, txout)| {
794 if i == output_idx || txout.is_p2a_fee_anchor() {
795 None
796 } else {
797 Some(txout.clone())
798 }
799 })
800 .collect(),
801 fee_amount: Amount::ZERO,
802 },
803 ]).collect(),
804 },
805 };
806
807 let spending_txid = self.unsigned_isolation_fanout_tx.as_ref()
808 .expect("isolation fanout tx must exist when isolated_outputs is non-empty")
809 .compute_txid();
810 ret.push((vtxo, spending_txid));
811 }
812
813 ret
814 }
815
816 pub fn input_spend_info(&self) -> (VtxoId, Txid) {
818 if let Some((_tx, checkpoint_txid)) = &self.checkpoint_data {
819 (self.input.id(), *checkpoint_txid)
820 } else {
821 (self.input.id(), self.unsigned_arkoor_txs[0].compute_txid())
822 }
823 }
824
825 pub fn build_unsigned_internal_vtxos(&self) -> Vec<(ServerVtxo<Full>, Txid)> {
828 self.build_internal_vtxos(None)
829 }
830
831 pub fn spend_info(&self) -> Vec<(VtxoId, Txid)> {
833 let mut ret = vec![self.input_spend_info()];
834 for (vtxo, spending_txid) in self.build_unsigned_internal_vtxos() {
835 ret.push((vtxo.id(), spending_txid));
836 }
837 ret
838 }
839
840 pub fn virtual_transactions(&self) -> Vec<Txid> {
845 let mut ret = Vec::new();
846 if let Some((_, txid)) = &self.checkpoint_data {
848 ret.push(*txid);
849 }
850 ret.extend(self.unsigned_arkoor_txs.iter().map(|tx| tx.compute_txid()));
852 if let Some(tx) = &self.unsigned_isolation_fanout_tx {
854 ret.push(tx.compute_txid());
855 }
856 ret
857 }
858
859 fn taptweak_at(&self, idx: usize) -> TapTweakHash {
860 if idx == 0 { self.input_tweak } else { self.checkpoint_policy_tweak }
861 }
862
863 fn user_pubkey(&self) -> PublicKey {
864 self.input.user_pubkey()
865 }
866
867 fn server_pubkey(&self) -> PublicKey {
868 self.input.server_pubkey()
869 }
870
871 fn construct_unsigned_checkpoint_tx<G>(
876 input: &Vtxo<G>,
877 outputs: &[ArkoorDestination],
878 dust_isolation_amount: Option<Amount>,
879 ) -> Transaction {
880
881 let output_policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
883 let checkpoint_spk = output_policy
884 .script_pubkey(input.server_pubkey(), input.exit_delta(), input.expiry_height());
885
886 Transaction {
887 version: bitcoin::transaction::Version(3),
888 lock_time: bitcoin::absolute::LockTime::ZERO,
889 input: vec![TxIn {
890 previous_output: input.point(),
891 script_sig: ScriptBuf::new(),
892 sequence: Sequence::ZERO,
893 witness: Witness::new(),
894 }],
895 output: outputs.iter().map(|o| {
896 TxOut {
897 value: o.total_amount,
898 script_pubkey: checkpoint_spk.clone(),
899 }
900 })
901 .chain(dust_isolation_amount.map(|amt| {
903 TxOut {
904 value: amt,
905 script_pubkey: checkpoint_spk.clone(),
906 }
907 }))
908 .chain([fee::fee_anchor()]).collect()
909 }
910 }
911
912 fn construct_unsigned_arkoor_txs<G>(
913 input: &Vtxo<G>,
914 outputs: &[ArkoorDestination],
915 checkpoint_txid: Option<Txid>,
916 dust_isolation_amount: Option<Amount>,
917 ) -> Vec<Transaction> {
918
919 if let Some(checkpoint_txid) = checkpoint_txid {
920 let mut arkoor_txs = Vec::with_capacity(outputs.len());
922
923 for (vout, output) in outputs.iter().enumerate() {
924 let transaction = Transaction {
925 version: bitcoin::transaction::Version(3),
926 lock_time: bitcoin::absolute::LockTime::ZERO,
927 input: vec![TxIn {
928 previous_output: OutPoint::new(checkpoint_txid, u32::try_from(vout).expect("output index fits in u32")),
929 script_sig: ScriptBuf::new(),
930 sequence: Sequence::ZERO,
931 witness: Witness::new(),
932 }],
933 output: vec![
934 output.policy.txout(
935 output.total_amount,
936 input.server_pubkey(),
937 input.exit_delta(),
938 input.expiry_height(),
939 ),
940 fee::fee_anchor(),
941 ]
942 };
943 arkoor_txs.push(transaction);
944 }
945
946 arkoor_txs
947 } else {
948 let checkpoint_policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
950 let checkpoint_spk = checkpoint_policy.script_pubkey(
951 input.server_pubkey(),
952 input.exit_delta(),
953 input.expiry_height()
954 );
955
956 let transaction = Transaction {
957 version: bitcoin::transaction::Version(3),
958 lock_time: bitcoin::absolute::LockTime::ZERO,
959 input: vec![TxIn {
960 previous_output: input.point(),
961 script_sig: ScriptBuf::new(),
962 sequence: Sequence::ZERO,
963 witness: Witness::new(),
964 }],
965 output: outputs.iter()
966 .map(|o| o.policy.txout(
967 o.total_amount,
968 input.server_pubkey(),
969 input.exit_delta(),
970 input.expiry_height(),
971 ))
972 .chain(dust_isolation_amount.map(|amt| TxOut {
974 value: amt,
975 script_pubkey: checkpoint_spk.clone(),
976 }))
977 .chain([fee::fee_anchor()])
978 .collect()
979 };
980 vec![transaction]
981 }
982 }
983
984 fn construct_unsigned_isolation_fanout_tx<G>(
992 input: &Vtxo<G>,
993 isolated_outputs: &[ArkoorDestination],
994 parent_txid: Txid, dust_isolation_output_vout: u32, ) -> Transaction {
997
998 Transaction {
999 version: bitcoin::transaction::Version(3),
1000 lock_time: bitcoin::absolute::LockTime::ZERO,
1001 input: vec![TxIn {
1002 previous_output: OutPoint::new(parent_txid, dust_isolation_output_vout),
1003 script_sig: ScriptBuf::new(),
1004 sequence: Sequence::ZERO,
1005 witness: Witness::new(),
1006 }],
1007 output: isolated_outputs.iter().map(|o| {
1008 TxOut {
1009 value: o.total_amount,
1010 script_pubkey: o.policy.script_pubkey(
1011 input.server_pubkey(),
1012 input.exit_delta(),
1013 input.expiry_height(),
1014 ),
1015 }
1016 }).chain([fee::fee_anchor()]).collect(),
1017 }
1018 }
1019
1020 fn validate_amounts<G>(
1021 input: &Vtxo<G>,
1022 outputs: &[ArkoorDestination],
1023 isolation_outputs: &[ArkoorDestination],
1024 ) -> Result<(), ArkoorConstructionError> {
1025
1026 let input_amount = input.amount();
1031
1032 let output_amount = outputs.iter().chain(isolation_outputs.iter())
1034 .map(|o| o.total_amount)
1035 .checked_sum()
1036 .ok_or(ArkoorConstructionError::Overflow)?;
1037
1038 if input_amount != output_amount {
1039 return Err(ArkoorConstructionError::Unbalanced {
1040 input: input_amount,
1041 output: output_amount,
1042 })
1043 }
1044
1045 if outputs.is_empty() {
1047 return Err(ArkoorConstructionError::NoOutputs)
1048 }
1049
1050 if outputs.iter().chain(isolation_outputs.iter())
1055 .any(|o| o.total_amount == Amount::ZERO)
1056 {
1057 return Err(ArkoorConstructionError::ZeroValueOutput)
1058 }
1059
1060 if outputs.len() > u8::MAX as usize || isolation_outputs.len() > u8::MAX as usize {
1062 return Err(ArkoorConstructionError::TooManyOutputs)
1063 }
1064
1065 if !isolation_outputs.is_empty() {
1069 let isolation_sum: Amount = isolation_outputs.iter()
1070 .map(|o| o.total_amount).sum();
1071 if isolation_sum < P2TR_DUST {
1072 return Err(ArkoorConstructionError::Dust)
1073 }
1074 }
1075
1076 if isolation_outputs.iter().any(|o| o.total_amount >= P2TR_DUST * 2) {
1079 return Err(ArkoorConstructionError::IsolationNotNeeded)
1080 }
1081
1082 if !isolation_outputs.is_empty()
1085 && isolation_outputs.iter().all(|o| o.total_amount >= P2TR_DUST)
1086 {
1087 return Err(ArkoorConstructionError::IsolationNotNeeded)
1088 }
1089
1090 if !isolation_outputs.is_empty()
1092 && outputs.iter().any(|o| o.total_amount < P2TR_DUST)
1093 {
1094 return Err(ArkoorConstructionError::Dust)
1095 }
1096
1097 if outputs.iter().any(|o| o.total_amount < P2TR_DUST)
1101 && outputs.iter().any(|o| o.total_amount >= P2TR_DUST * 2)
1102 {
1103 return Err(ArkoorConstructionError::Dust)
1104 }
1105
1106 Ok(())
1107 }
1108
1109
1110 fn to_state<S2: state::BuilderState>(self) -> ArkoorBuilder<S2> {
1111 ArkoorBuilder {
1112 input: self.input,
1113 outputs: self.outputs,
1114 isolated_outputs: self.isolated_outputs,
1115 checkpoint_data: self.checkpoint_data,
1116 unsigned_arkoor_txs: self.unsigned_arkoor_txs,
1117 unsigned_isolation_fanout_tx: self.unsigned_isolation_fanout_tx,
1118 new_vtxo_ids: self.new_vtxo_ids,
1119 sighashes: self.sighashes,
1120 input_tweak: self.input_tweak,
1121 checkpoint_policy_tweak: self.checkpoint_policy_tweak,
1122 user_keypair: self.user_keypair,
1123 user_pub_nonces: self.user_pub_nonces,
1124 user_sec_nonces: self.user_sec_nonces,
1125 server_pub_nonces: self.server_pub_nonces,
1126 server_partial_sigs: self.server_partial_sigs,
1127 full_signatures: self.full_signatures,
1128 _state: PhantomData,
1129 }
1130 }
1131}
1132
1133impl ArkoorBuilder<state::Initial> {
1134 pub fn new_with_checkpoint(
1136 input: Vtxo<Full>,
1137 outputs: Vec<ArkoorDestination>,
1138 isolated_outputs: Vec<ArkoorDestination>,
1139 ) -> Result<Self, ArkoorConstructionError> {
1140 Self::new(input, outputs, isolated_outputs, true)
1141 }
1142
1143 pub fn new_without_checkpoint(
1145 input: Vtxo<Full>,
1146 outputs: Vec<ArkoorDestination>,
1147 isolated_outputs: Vec<ArkoorDestination>,
1148 ) -> Result<Self, ArkoorConstructionError> {
1149 Self::new(input, outputs, isolated_outputs, false)
1150 }
1151
1152 pub fn new_with_checkpoint_isolate_dust(
1158 input: Vtxo<Full>,
1159 outputs: Vec<ArkoorDestination>,
1160 ) -> Result<Self, ArkoorConstructionError> {
1161 Self::new_isolate_dust(input, outputs, true)
1162 }
1163
1164 pub(crate) fn new_isolate_dust(
1165 input: Vtxo<Full>,
1166 outputs: Vec<ArkoorDestination>,
1167 use_checkpoints: bool,
1168 ) -> Result<Self, ArkoorConstructionError> {
1169 if outputs.iter().all(|v| v.total_amount >= P2TR_DUST)
1171 || outputs.iter().all(|v| v.total_amount < P2TR_DUST)
1172 {
1173 return Self::new(input, outputs, vec![], use_checkpoints);
1174 }
1175
1176 let (mut dust, mut non_dust) = outputs.iter().cloned()
1178 .partition::<Vec<_>, _>(|v| v.total_amount < P2TR_DUST);
1179
1180 let dust_sum = dust.iter().map(|o| o.total_amount).sum::<Amount>();
1181 if dust_sum >= P2TR_DUST {
1182 return Self::new(input, non_dust, dust, use_checkpoints);
1183 }
1184
1185 let non_dust_sum = non_dust.iter().map(|o| o.total_amount).sum::<Amount>();
1187 if non_dust_sum < P2TR_DUST * 2 {
1188 return Self::new(input, outputs, vec![], use_checkpoints);
1189 }
1190
1191 let deficit = P2TR_DUST - dust_sum;
1193 let split_idx = non_dust.iter()
1196 .position(|o| o.total_amount - deficit >= P2TR_DUST);
1197
1198 if let Some(idx) = split_idx {
1199 let output_to_split = non_dust[idx].clone();
1200
1201 let dust_piece = ArkoorDestination {
1202 total_amount: deficit,
1203 policy: output_to_split.policy.clone(),
1204 };
1205 let leftover = ArkoorDestination {
1206 total_amount: output_to_split.total_amount - deficit,
1207 policy: output_to_split.policy,
1208 };
1209
1210 non_dust[idx] = leftover;
1211 dust.insert(0, dust_piece);
1213
1214 return Self::new(input, non_dust, dust, use_checkpoints);
1215 } else {
1216 let all_outputs = non_dust.into_iter().chain(dust).collect();
1218 return Self::new(input, all_outputs, vec![], use_checkpoints);
1219 }
1220 }
1221
1222 pub(crate) fn new(
1223 input: Vtxo<Full>,
1224 outputs: Vec<ArkoorDestination>,
1225 isolated_outputs: Vec<ArkoorDestination>,
1226 use_checkpoint: bool,
1227 ) -> Result<Self, ArkoorConstructionError> {
1228 Self::validate_amounts(&input, &outputs, &isolated_outputs)?;
1230
1231 let combined_dust_amount = if !isolated_outputs.is_empty() {
1233 Some(isolated_outputs.iter().map(|o| o.total_amount).sum())
1234 } else {
1235 None
1236 };
1237
1238 let unsigned_checkpoint_tx = if use_checkpoint {
1240 let tx = Self::construct_unsigned_checkpoint_tx(
1241 &input,
1242 &outputs,
1243 combined_dust_amount,
1244 );
1245 let txid = tx.compute_txid();
1246 Some((tx, txid))
1247 } else {
1248 None
1249 };
1250
1251 let unsigned_arkoor_txs = Self::construct_unsigned_arkoor_txs(
1253 &input,
1254 &outputs,
1255 unsigned_checkpoint_tx.as_ref().map(|t| t.1),
1256 combined_dust_amount,
1257 );
1258
1259 let unsigned_isolation_fanout_tx = if !isolated_outputs.is_empty() {
1261 let dust_isolation_output_vout = u32::try_from(outputs.len())
1264 .expect("output count fits in u32");
1265
1266 let parent_txid = if let Some((_tx, txid)) = &unsigned_checkpoint_tx {
1267 *txid
1268 } else {
1269 unsigned_arkoor_txs[0].compute_txid()
1270 };
1271
1272 Some(Self::construct_unsigned_isolation_fanout_tx(
1273 &input,
1274 &isolated_outputs,
1275 parent_txid,
1276 dust_isolation_output_vout,
1277 ))
1278 } else {
1279 None
1280 };
1281
1282 let new_vtxo_ids = unsigned_arkoor_txs.iter()
1284 .map(|tx| OutPoint::new(tx.compute_txid(), 0))
1285 .map(|outpoint| VtxoId::from(outpoint))
1286 .collect();
1287
1288 let mut sighashes = Vec::new();
1290
1291 if let Some((checkpoint_tx, _txid)) = &unsigned_checkpoint_tx {
1292 sighashes.push(arkoor_sighash(&input.txout(), checkpoint_tx));
1294
1295 for vout in 0..outputs.len() {
1297 let prevout = checkpoint_tx.output[vout].clone();
1298 sighashes.push(arkoor_sighash(&prevout, &unsigned_arkoor_txs[vout]));
1299 }
1300 } else {
1301 sighashes.push(arkoor_sighash(&input.txout(), &unsigned_arkoor_txs[0]));
1303 }
1304
1305 if let Some(ref tx) = unsigned_isolation_fanout_tx {
1307 let dust_output_vout = outputs.len(); let prevout = if let Some((checkpoint_tx, _txid)) = &unsigned_checkpoint_tx {
1309 checkpoint_tx.output[dust_output_vout].clone()
1310 } else {
1311 unsigned_arkoor_txs[0].output[dust_output_vout].clone()
1313 };
1314 sighashes.push(arkoor_sighash(&prevout, tx));
1315 }
1316
1317 let policy = ServerVtxoPolicy::new_checkpoint(input.user_pubkey());
1319 let input_tweak = input.output_taproot().tap_tweak();
1320 let checkpoint_policy_tweak = policy.taproot(
1321 input.server_pubkey(),
1322 input.exit_delta(),
1323 input.expiry_height(),
1324 ).tap_tweak();
1325
1326 Ok(Self {
1327 input: input,
1328 outputs: outputs,
1329 isolated_outputs,
1330 sighashes: sighashes,
1331 input_tweak,
1332 checkpoint_policy_tweak,
1333 checkpoint_data: unsigned_checkpoint_tx,
1334 unsigned_arkoor_txs: unsigned_arkoor_txs,
1335 unsigned_isolation_fanout_tx,
1336 new_vtxo_ids: new_vtxo_ids,
1337 user_keypair: None,
1338 user_pub_nonces: None,
1339 user_sec_nonces: None,
1340 server_pub_nonces: None,
1341 server_partial_sigs: None,
1342 full_signatures: None,
1343 _state: PhantomData,
1344 })
1345 }
1346
1347 pub fn generate_user_nonces(
1350 mut self,
1351 user_keypair: Keypair,
1352 ) -> ArkoorBuilder<state::UserGeneratedNonces> {
1353 let mut user_pub_nonces = Vec::with_capacity(self.nb_sigs());
1354 let mut user_sec_nonces = Vec::with_capacity(self.nb_sigs());
1355
1356 for idx in 0..self.nb_sigs() {
1357 let sighash = &self.sighashes[idx].to_byte_array();
1358 let (sec_nonce, pub_nonce) = musig::nonce_pair_with_msg(&user_keypair, sighash);
1359
1360 user_pub_nonces.push(pub_nonce);
1361 user_sec_nonces.push(sec_nonce);
1362 }
1363
1364 self.user_keypair = Some(user_keypair);
1365 self.user_pub_nonces = Some(user_pub_nonces);
1366 self.user_sec_nonces = Some(user_sec_nonces);
1367
1368 self.to_state::<state::UserGeneratedNonces>()
1369 }
1370
1371 fn set_user_pub_nonces(
1378 mut self,
1379 user_pub_nonces: Vec<musig::PublicNonce>,
1380 ) -> Result<ArkoorBuilder<state::ServerCanCosign>, ArkoorSigningError> {
1381 if user_pub_nonces.len() != self.nb_sigs() {
1382 return Err(ArkoorSigningError::InvalidNbUserNonces {
1383 expected: self.nb_sigs(),
1384 got: user_pub_nonces.len()
1385 })
1386 }
1387
1388 self.user_pub_nonces = Some(user_pub_nonces);
1389 Ok(self.to_state::<state::ServerCanCosign>())
1390 }
1391
1392 pub fn cosign_both(
1397 mut self,
1398 user_keypair: &Keypair,
1399 server_keypair: &Keypair,
1400 ) -> Result<ArkoorBuilder<state::UserSigned>, ArkoorSigningError> {
1401 if user_keypair.public_key() != self.input.user_pubkey() {
1402 return Err(ArkoorSigningError::IncorrectKey {
1403 expected: self.input.user_pubkey(),
1404 got: user_keypair.public_key(),
1405 });
1406 }
1407 if server_keypair.public_key() != self.input.server_pubkey() {
1408 return Err(ArkoorSigningError::IncorrectKey {
1409 expected: self.input.server_pubkey(),
1410 got: server_keypair.public_key(),
1411 });
1412 }
1413
1414 let mut sigs = Vec::with_capacity(self.nb_sigs());
1415 for idx in 0..self.nb_sigs() {
1416 sigs.push(musig::cosign_both(
1417 user_keypair,
1418 server_keypair,
1419 self.sighashes[idx].to_byte_array(),
1420 Some(self.taptweak_at(idx).to_byte_array()),
1421 ));
1422 }
1423
1424 self.full_signatures = Some(sigs);
1425 Ok(self.to_state::<state::UserSigned>())
1426 }
1427}
1428
1429impl<'a> ArkoorBuilder<state::ServerCanCosign> {
1430 pub fn from_cosign_request(
1431 cosign_request: ArkoorCosignRequest<Vtxo<Full>>,
1432 ) -> Result<ArkoorBuilder<state::ServerCanCosign>, ArkoorSigningError> {
1433 cosign_request.verify_attestation()
1434 .map_err(ArkoorSigningError::InvalidAttestation)?;
1435
1436 let ret = ArkoorBuilder::new(
1437 cosign_request.input,
1438 cosign_request.outputs,
1439 cosign_request.isolated_outputs,
1440 cosign_request.use_checkpoint,
1441 )
1442 .map_err(ArkoorSigningError::ArkoorConstructionError)?
1443 .set_user_pub_nonces(cosign_request.user_pub_nonces.clone())?;
1444 Ok(ret)
1445 }
1446
1447 pub fn server_cosign(
1448 mut self,
1449 server_keypair: &Keypair,
1450 ) -> Result<ArkoorBuilder<state::ServerSigned>, ArkoorSigningError> {
1451 if server_keypair.public_key() != self.input.server_pubkey() {
1453 return Err(ArkoorSigningError::IncorrectKey {
1454 expected: self.input.server_pubkey(),
1455 got: server_keypair.public_key(),
1456 });
1457 }
1458
1459 let mut server_pub_nonces = Vec::with_capacity(self.outputs.len().saturating_add(1));
1460 let mut server_partial_sigs = Vec::with_capacity(self.outputs.len().saturating_add(1));
1461
1462 for idx in 0..self.nb_sigs() {
1463 let (server_pub_nonce, server_partial_sig) = musig::deterministic_partial_sign(
1464 &server_keypair,
1465 [self.input.user_pubkey()],
1466 &[&self.user_pub_nonces.as_ref().expect("state-invariant")[idx]],
1467 self.sighashes[idx].to_byte_array(),
1468 Some(self.taptweak_at(idx).to_byte_array()),
1469 );
1470
1471 server_pub_nonces.push(server_pub_nonce);
1472 server_partial_sigs.push(server_partial_sig);
1473 };
1474
1475 self.server_pub_nonces = Some(server_pub_nonces);
1476 self.server_partial_sigs = Some(server_partial_sigs);
1477 Ok(self.to_state::<state::ServerSigned>())
1478 }
1479}
1480
1481impl ArkoorBuilder<state::ServerSigned> {
1482 pub fn user_pub_nonces(&self) -> Vec<musig::PublicNonce> {
1483 self.user_pub_nonces.as_ref().expect("state invariant").clone()
1484 }
1485
1486 pub fn server_partial_signatures(&self) -> Vec<musig::PartialSignature> {
1487 self.server_partial_sigs.as_ref().expect("state invariant").clone()
1488 }
1489
1490 pub fn cosign_response(&self) -> ArkoorCosignResponse {
1491 ArkoorCosignResponse {
1492 server_pub_nonces: self.server_pub_nonces.as_ref()
1493 .expect("state invariant").clone(),
1494 server_partial_sigs: self.server_partial_sigs.as_ref()
1495 .expect("state invariant").clone(),
1496 }
1497 }
1498}
1499
1500impl ArkoorBuilder<state::UserGeneratedNonces> {
1501 pub fn user_pub_nonces(&self) -> &[PublicNonce] {
1502 self.user_pub_nonces.as_ref().expect("State invariant")
1503 }
1504
1505 pub fn cosign_request(&self) -> ArkoorCosignRequest<Vtxo<Full>> {
1506 ArkoorCosignRequest::new(
1507 self.user_pub_nonces().to_vec(),
1508 self.input.clone(),
1509 self.outputs.clone(),
1510 self.isolated_outputs.clone(),
1511 self.checkpoint_data.is_some(),
1512 self.user_keypair.as_ref().expect("State invariant"),
1513 )
1514 }
1515
1516 fn validate_server_cosign_response(
1517 &self,
1518 data: &ArkoorCosignResponse,
1519 ) -> Result<(), ArkoorSigningError> {
1520
1521 if data.server_pub_nonces.len() != self.nb_sigs() {
1523 return Err(ArkoorSigningError::InvalidNbServerNonces {
1524 expected: self.nb_sigs(),
1525 got: data.server_pub_nonces.len(),
1526 });
1527 }
1528
1529 if data.server_partial_sigs.len() != self.nb_sigs() {
1530 return Err(ArkoorSigningError::InvalidNbServerPartialSigs {
1531 expected: self.nb_sigs(),
1532 got: data.server_partial_sigs.len(),
1533 })
1534 }
1535
1536 for idx in 0..self.nb_sigs() {
1538 let is_valid_sig = scripts::verify_partial_sig(
1539 self.sighashes[idx],
1540 self.taptweak_at(idx),
1541 (self.input.server_pubkey(), &data.server_pub_nonces[idx]),
1542 (self.input.user_pubkey(), &self.user_pub_nonces()[idx]),
1543 &data.server_partial_sigs[idx]
1544 );
1545
1546 if !is_valid_sig {
1547 return Err(ArkoorSigningError::InvalidPartialSignature {
1548 index: idx,
1549 });
1550 }
1551 }
1552 Ok(())
1553 }
1554
1555 pub fn user_cosign(
1556 mut self,
1557 user_keypair: &Keypair,
1558 server_cosign_data: &ArkoorCosignResponse,
1559 ) -> Result<ArkoorBuilder<state::UserSigned>, ArkoorSigningError> {
1560 if user_keypair.public_key() != self.input.user_pubkey() {
1562 return Err(ArkoorSigningError::IncorrectKey {
1563 expected: self.input.user_pubkey(),
1564 got: user_keypair.public_key(),
1565 });
1566 }
1567
1568 self.validate_server_cosign_response(&server_cosign_data)?;
1570
1571 let mut sigs = Vec::with_capacity(self.nb_sigs());
1572
1573 let user_sec_nonces = self.user_sec_nonces.take().expect("state invariant");
1576
1577 for (idx, user_sec_nonce) in user_sec_nonces.into_iter().enumerate() {
1578 let user_pub_nonce = self.user_pub_nonces()[idx];
1579 let server_pub_nonce = server_cosign_data.server_pub_nonces[idx];
1580 let agg_nonce = musig::nonce_agg(&[&user_pub_nonce, &server_pub_nonce]);
1581
1582 let (_partial, maybe_sig) = musig::partial_sign(
1583 [self.user_pubkey(), self.server_pubkey()],
1584 agg_nonce,
1585 &user_keypair,
1586 user_sec_nonce,
1587 self.sighashes[idx].to_byte_array(),
1588 Some(self.taptweak_at(idx).to_byte_array()),
1589 Some(&[&server_cosign_data.server_partial_sigs[idx]])
1590 );
1591
1592 let sig = maybe_sig.expect("The full signature exists. The server did sign first");
1593 sigs.push(sig);
1594 }
1595
1596 self.full_signatures = Some(sigs);
1597
1598 Ok(self.to_state::<state::UserSigned>())
1599 }
1600}
1601
1602
1603impl<'a> ArkoorBuilder<state::UserSigned> {
1604 pub fn build_signed_vtxos(&self) -> Vec<Vtxo<Full>> {
1605 let sigs = self.full_signatures.as_ref().expect("state invariant");
1606 let mut ret = Vec::with_capacity(self.outputs.len().saturating_add(self.isolated_outputs.len()));
1607
1608 if self.checkpoint_data.is_some() {
1609 let checkpoint_sig = sigs[0];
1610
1611 for i in 0..self.outputs.len() {
1613 let arkoor_sig = sigs[i.saturating_add(1)];
1614 ret.push(self.build_vtxo_at(i, Some(checkpoint_sig), Some(arkoor_sig)));
1615 }
1616
1617 if self.unsigned_isolation_fanout_tx.is_some() {
1619 let m = self.outputs.len();
1620 let fanout_tx_sig = sigs[m.saturating_add(1)];
1621
1622 for i in 0..self.isolated_outputs.len() {
1623 ret.push(self.build_isolated_vtxo_at(
1624 i,
1625 Some(checkpoint_sig),
1626 Some(fanout_tx_sig),
1627 ));
1628 }
1629 }
1630 } else {
1631 let arkoor_sig = sigs[0];
1633
1634 for i in 0..self.outputs.len() {
1636 ret.push(self.build_vtxo_at(i, None, Some(arkoor_sig)));
1637 }
1638
1639 if self.unsigned_isolation_fanout_tx.is_some() {
1641 let fanout_tx_sig = sigs[1];
1642
1643 for i in 0..self.isolated_outputs.len() {
1644 ret.push(self.build_isolated_vtxo_at(
1645 i,
1646 Some(arkoor_sig), Some(fanout_tx_sig),
1648 ));
1649 }
1650 }
1651 }
1652
1653 ret
1654 }
1655
1656 pub fn signed_virtual_transactions(&self) -> Vec<Transaction> {
1661 let sigs = self.full_signatures.as_ref().expect("state invariant");
1662 let mut ret = Vec::new();
1663 let mut sig_idx = 0usize;
1664 if let Some((tx, _)) = &self.checkpoint_data {
1665 let mut tx = tx.clone();
1666 tx.input[0].witness.push(&sigs[sig_idx][..]);
1667 ret.push(tx);
1668 sig_idx = sig_idx.saturating_add(1);
1669 }
1670 for tx in &self.unsigned_arkoor_txs {
1671 let mut tx = tx.clone();
1672 tx.input[0].witness.push(&sigs[sig_idx][..]);
1673 ret.push(tx);
1674 sig_idx = sig_idx.saturating_add(1);
1675 }
1676 if let Some(tx) = &self.unsigned_isolation_fanout_tx {
1677 let mut tx = tx.clone();
1678 tx.input[0].witness.push(&sigs[sig_idx][..]);
1679 ret.push(tx);
1680 }
1681 ret
1682 }
1683
1684 pub fn build_signed_internal_vtxos(&self) -> Vec<(ServerVtxo<Full>, Txid)> {
1687 let sigs = self.full_signatures.as_ref().expect("state invariant");
1688 let intermediate_sig = if self.checkpoint_data.is_some() || !self.isolated_outputs.is_empty() {
1689 Some(sigs[0])
1690 } else {
1691 None
1692 };
1693 self.build_internal_vtxos(intermediate_sig)
1694 }
1695}
1696
1697fn arkoor_sighash(prevout: &TxOut, arkoor_tx: &Transaction) -> TapSighash {
1698 let mut shc = SighashCache::new(arkoor_tx);
1699
1700 shc.taproot_key_spend_signature_hash(
1701 0, &sighash::Prevouts::All(&[prevout]), TapSighashType::Default,
1702 ).expect("sighash error")
1703}
1704
1705#[cfg(test)]
1706mod test {
1707 use super::*;
1708
1709 use std::collections::HashSet;
1710
1711 use bitcoin::Amount;
1712 use bitcoin::secp256k1::Keypair;
1713 use bitcoin::secp256k1::rand;
1714 use bitcoin::secp256k1::rand::{Rng, SeedableRng};
1715
1716 use bitcoin_ext::{BlockDelta, BlockHeight};
1717
1718 use crate::SECP;
1719 use crate::test_util::dummy::DummyTestVtxoSpec;
1720 use crate::vtxo::VtxoId;
1721
1722 fn verify_signed_internal_vtxos(
1724 builder: &ArkoorBuilder<state::UserSigned>,
1725 funding_tx: &Transaction,
1726 ) {
1727 let signed = builder.build_signed_internal_vtxos();
1728 let unsigned = builder.build_unsigned_internal_vtxos();
1729 assert_eq!(signed.len(), unsigned.len());
1730
1731 for (vtxo, _spending_txid) in &signed {
1732 vtxo.validate(funding_tx).expect("signed internal vtxo must be valid");
1733 }
1734 }
1735
1736 fn verify_builder<S: state::BuilderState>(
1738 builder: &ArkoorBuilder<S>,
1739 input: &Vtxo<Full>,
1740 outputs: &[ArkoorDestination],
1741 isolated_outputs: &[ArkoorDestination],
1742 ) {
1743 let has_isolation = !isolated_outputs.is_empty();
1744
1745 let spend_info = builder.spend_info();
1746 let spend_vtxo_ids: HashSet<VtxoId> = spend_info.iter().map(|(id, _)| *id).collect();
1747
1748 assert_eq!(spend_info[0].0, input.id());
1750
1751 assert_eq!(spend_vtxo_ids.len(), spend_info.len());
1753
1754 let internal_vtxos = builder.build_unsigned_internal_vtxos();
1756 let internal_vtxo_ids = internal_vtxos.iter().map(|(v, _)| v.id()).collect::<HashSet<_>>();
1757 for (internal_vtxo, _spending_txid) in &internal_vtxos {
1758 assert!(spend_vtxo_ids.contains(&internal_vtxo.id()));
1759 assert!(matches!(internal_vtxo.policy(), ServerVtxoPolicy::Checkpoint(_)));
1760 }
1761
1762 for (vtxo_id, _) in &spend_info[1..] {
1764 assert!(internal_vtxo_ids.contains(vtxo_id));
1765 }
1766
1767 if has_isolation {
1769 let (isolation_vtxo, _) = internal_vtxos.last().unwrap();
1770 let expected_isolation_amount: Amount = isolated_outputs.iter()
1771 .map(|o| o.total_amount)
1772 .sum();
1773 assert_eq!(isolation_vtxo.amount(), expected_isolation_amount);
1774 }
1775
1776 let final_vtxos = builder.build_unsigned_vtxos().collect::<Vec<_>>();
1778 for final_vtxo in &final_vtxos {
1779 assert!(!spend_vtxo_ids.contains(&final_vtxo.id()));
1780 }
1781
1782 let all_destinations = outputs.iter()
1784 .chain(isolated_outputs.iter())
1785 .collect::<Vec<&_>>();
1786 for (vtxo, dest) in final_vtxos.iter().zip(all_destinations.iter()) {
1787 assert_eq!(vtxo.amount(), dest.total_amount);
1788 assert_eq!(vtxo.policy, dest.policy);
1789 }
1790
1791 let total_output_amount: Amount = final_vtxos.iter().map(|v| v.amount()).sum();
1793 assert_eq!(total_output_amount, input.amount());
1794 }
1795
1796 #[test]
1797 fn build_checkpointed_arkoor() {
1798 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1799 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1800 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1801
1802 println!("Alice keypair: {}", alice_keypair.public_key());
1803 println!("Bob keypair: {}", bob_keypair.public_key());
1804 println!("Server keypair: {}", server_keypair.public_key());
1805 println!("-----------------------------------------------");
1806
1807 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1808 amount: Amount::from_sat(100_330),
1809 fee: Amount::from_sat(330),
1810 expiry_height: BlockHeight::new(1000),
1811 exit_delta: BlockDelta::new(128),
1812 user_keypair: alice_keypair.clone(),
1813 server_keypair: server_keypair.clone()
1814 }.build();
1815
1816 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1818
1819 let dest = vec![
1820 ArkoorDestination {
1821 total_amount: Amount::from_sat(96_000),
1822 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1823 },
1824 ArkoorDestination {
1825 total_amount: Amount::from_sat(4_000),
1826 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1827 }
1828 ];
1829
1830 let user_builder = ArkoorBuilder::new_with_checkpoint(
1831 alice_vtxo.clone(),
1832 dest.clone(),
1833 vec![], ).expect("Valid arkoor request");
1835
1836 verify_builder(&user_builder, &alice_vtxo, &dest, &[]);
1837
1838 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1839 let cosign_request = user_builder.cosign_request();
1840
1841 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1843 .expect("Invalid cosign request")
1844 .server_cosign(&server_keypair)
1845 .expect("Incorrect key");
1846
1847 let cosign_data = server_builder.cosign_response();
1848
1849 let signed_builder = user_builder
1851 .user_cosign(&alice_keypair, &cosign_data)
1852 .expect("Valid cosign data and correct key");
1853 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
1854 let vtxos = signed_builder.build_signed_vtxos();
1855
1856 for vtxo in vtxos.into_iter() {
1857 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1859
1860 let mut prev_tx = funding_tx.clone();
1862 for tx in vtxo.transactions().map(|item| item.tx) {
1863 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1864 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1865 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1866 prev_tx = tx;
1867 }
1868 }
1869
1870 }
1871
1872 #[test]
1873 fn build_checkpointed_arkoor_with_dust_isolation() {
1874 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1877 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1878 let charlie_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1879 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1880
1881 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1882 amount: Amount::from_sat(100_330),
1883 fee: Amount::from_sat(330),
1884 expiry_height: BlockHeight::new(1000),
1885 exit_delta: BlockDelta::new(128),
1886 user_keypair: alice_keypair.clone(),
1887 server_keypair: server_keypair.clone()
1888 }.build();
1889
1890 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1892
1893 let outputs = vec![
1895 ArkoorDestination {
1896 total_amount: Amount::from_sat(99_600),
1897 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1898 },
1899 ];
1900
1901 let dust_outputs = vec![
1903 ArkoorDestination {
1904 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(charlie_keypair.public_key())
1906 },
1907 ArkoorDestination {
1908 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1910 }
1911 ];
1912
1913 let user_builder = ArkoorBuilder::new_with_checkpoint(
1914 alice_vtxo.clone(),
1915 outputs.clone(),
1916 dust_outputs.clone(),
1917 ).expect("Valid arkoor request with dust isolation");
1918
1919 verify_builder(&user_builder, &alice_vtxo, &outputs, &dust_outputs);
1920
1921 assert!(
1923 user_builder.unsigned_isolation_fanout_tx.is_some(),
1924 "Dust isolation should be active",
1925 );
1926
1927 assert_eq!(user_builder.nb_sigs(), 3);
1929
1930 let user_builder = user_builder.generate_user_nonces(alice_keypair);
1931 let cosign_request = user_builder.cosign_request();
1932
1933 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
1935 .expect("Invalid cosign request")
1936 .server_cosign(&server_keypair)
1937 .expect("Incorrect key");
1938
1939 let cosign_data = server_builder.cosign_response();
1940
1941 let signed_builder = user_builder
1943 .user_cosign(&alice_keypair, &cosign_data)
1944 .expect("Valid cosign data and correct key");
1945 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
1946 let vtxos = signed_builder.build_signed_vtxos();
1947
1948 assert_eq!(vtxos.len(), 3);
1950
1951 for vtxo in vtxos.into_iter() {
1952 vtxo.validate(&funding_tx).expect("Invalid VTXO");
1954
1955 let mut prev_tx = funding_tx.clone();
1957 for tx in vtxo.transactions().map(|item| item.tx) {
1958 let prev_outpoint: OutPoint = tx.input[0].previous_output;
1959 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
1960 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
1961 prev_tx = tx;
1962 }
1963 }
1964 }
1965
1966 #[test]
1967 fn build_no_checkpoint_arkoor() {
1968 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1969 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1970 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
1971
1972 println!("Alice keypair: {}", alice_keypair.public_key());
1973 println!("Bob keypair: {}", bob_keypair.public_key());
1974 println!("Server keypair: {}", server_keypair.public_key());
1975 println!("-----------------------------------------------");
1976
1977 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
1978 amount: Amount::from_sat(100_330),
1979 fee: Amount::from_sat(330),
1980 expiry_height: BlockHeight::new(1000),
1981 exit_delta: BlockDelta::new(128),
1982 user_keypair: alice_keypair.clone(),
1983 server_keypair: server_keypair.clone()
1984 }.build();
1985
1986 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
1988
1989 let dest = vec![
1990 ArkoorDestination {
1991 total_amount: Amount::from_sat(96_000),
1992 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
1993 },
1994 ArkoorDestination {
1995 total_amount: Amount::from_sat(4_000),
1996 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
1997 }
1998 ];
1999
2000 let user_builder = ArkoorBuilder::new_without_checkpoint(
2001 alice_vtxo.clone(),
2002 dest.clone(),
2003 vec![], ).expect("Valid arkoor request");
2005
2006 verify_builder(&user_builder, &alice_vtxo, &dest, &[]);
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 for vtxo in vtxos.into_iter() {
2027 vtxo.validate(&funding_tx).expect("Invalid VTXO");
2029
2030 let mut prev_tx = funding_tx.clone();
2032 for tx in vtxo.transactions().map(|item| item.tx) {
2033 let prev_outpoint: OutPoint = tx.input[0].previous_output;
2034 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
2035 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
2036 prev_tx = tx;
2037 }
2038 }
2039
2040 }
2041
2042 #[test]
2043 fn build_no_checkpoint_arkoor_with_dust_isolation() {
2044 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2047 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2048 let charlie_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2049 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2050
2051 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2052 amount: Amount::from_sat(100_330),
2053 fee: Amount::from_sat(330),
2054 expiry_height: BlockHeight::new(1000),
2055 exit_delta: BlockDelta::new(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");
2062
2063 let outputs = vec![
2065 ArkoorDestination {
2066 total_amount: Amount::from_sat(99_600),
2067 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2068 },
2069 ];
2070
2071 let dust_outputs = vec![
2073 ArkoorDestination {
2074 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(charlie_keypair.public_key())
2076 },
2077 ArkoorDestination {
2078 total_amount: Amount::from_sat(200), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2080 }
2081 ];
2082
2083 let user_builder = ArkoorBuilder::new_without_checkpoint(
2084 alice_vtxo.clone(),
2085 outputs.clone(),
2086 dust_outputs.clone(),
2087 ).expect("Valid arkoor request with dust isolation");
2088
2089 verify_builder(&user_builder, &alice_vtxo, &outputs, &dust_outputs);
2090
2091 assert!(
2093 user_builder.unsigned_isolation_fanout_tx.is_some(),
2094 "Dust isolation should be active",
2095 );
2096
2097 assert_eq!(user_builder.nb_sigs(), 2);
2100
2101 let user_builder = user_builder.generate_user_nonces(alice_keypair);
2102 let cosign_request = user_builder.cosign_request();
2103
2104 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
2106 .expect("Invalid cosign request")
2107 .server_cosign(&server_keypair)
2108 .expect("Incorrect key");
2109
2110 let cosign_data = server_builder.cosign_response();
2111
2112 let signed_builder = user_builder
2114 .user_cosign(&alice_keypair, &cosign_data)
2115 .expect("Valid cosign data and correct key");
2116 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
2117 let vtxos = signed_builder.build_signed_vtxos();
2118
2119 assert_eq!(vtxos.len(), 3);
2121
2122 for vtxo in vtxos.into_iter() {
2123 vtxo.validate(&funding_tx).expect("Invalid VTXO");
2125
2126 let mut prev_tx = funding_tx.clone();
2128 for tx in vtxo.transactions().map(|item| item.tx) {
2129 let prev_outpoint: OutPoint = tx.input[0].previous_output;
2130 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
2131 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
2132 prev_tx = tx;
2133 }
2134 }
2135 }
2136
2137 #[test]
2138 fn build_checkpointed_arkoor_outputs_must_be_above_dust_if_mixed() {
2139 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2141 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2142 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2143
2144 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2145 amount: Amount::from_sat(1_330),
2146 fee: Amount::from_sat(330),
2147 expiry_height: BlockHeight::new(1000),
2148 exit_delta: BlockDelta::new(128),
2149 user_keypair: alice_keypair.clone(),
2150 server_keypair: server_keypair.clone()
2151 }.build();
2152
2153 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
2154
2155 ArkoorBuilder::new_with_checkpoint(
2157 alice_vtxo.clone(),
2158 vec![
2159 ArkoorDestination {
2160 total_amount: Amount::from_sat(100), policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2162 }; 10
2163 ],
2164 vec![],
2165 ).unwrap();
2166
2167 let res_empty = ArkoorBuilder::new_with_checkpoint(
2169 alice_vtxo.clone(),
2170 vec![],
2171 vec![
2172 ArkoorDestination {
2173 total_amount: Amount::from_sat(100), policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2175 }; 10
2176 ],
2177 );
2178 match res_empty {
2179 Err(ArkoorConstructionError::NoOutputs) => {},
2180 _ => panic!("Expected NoOutputs error for empty outputs"),
2181 }
2182
2183 ArkoorBuilder::new_with_checkpoint(
2185 alice_vtxo.clone(),
2186 vec![
2187 ArkoorDestination {
2188 total_amount: Amount::from_sat(330), policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2190 }; 2
2191 ],
2192 vec![
2193 ArkoorDestination {
2194 total_amount: Amount::from_sat(170),
2195 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2196 }; 2
2197 ],
2198 ).unwrap();
2199
2200 let res_mixed_small = ArkoorBuilder::new_with_checkpoint(
2202 alice_vtxo.clone(),
2203 vec![
2204 ArkoorDestination {
2205 total_amount: Amount::from_sat(500),
2206 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2207 },
2208 ArkoorDestination {
2209 total_amount: Amount::from_sat(300),
2210 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2211 }
2212 ],
2213 vec![
2214 ArkoorDestination {
2215 total_amount: Amount::from_sat(100),
2216 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2217 }; 2 ],
2219 );
2220 match res_mixed_small {
2221 Err(ArkoorConstructionError::Dust) => {},
2222 _ => panic!("Expected Dust error for isolation sum < 330"),
2223 }
2224 }
2225
2226 #[test]
2227 fn spend_dust_vtxo() {
2228 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2230 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2231 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2232
2233 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2235 amount: Amount::from_sat(200),
2236 fee: Amount::ZERO,
2237 expiry_height: BlockHeight::new(1000),
2238 exit_delta: BlockDelta::new(128),
2239 user_keypair: alice_keypair.clone(),
2240 server_keypair: server_keypair.clone()
2241 }.build();
2242
2243 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
2244
2245 let dust_outputs = vec![
2248 ArkoorDestination {
2249 total_amount: Amount::from_sat(100),
2250 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2251 },
2252 ArkoorDestination {
2253 total_amount: Amount::from_sat(100),
2254 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2255 }
2256 ];
2257
2258 let user_builder = ArkoorBuilder::new_with_checkpoint(
2259 alice_vtxo.clone(),
2260 dust_outputs,
2261 vec![],
2262 ).expect("Valid arkoor request for all-dust case");
2263
2264 assert!(
2266 user_builder.unsigned_isolation_fanout_tx.is_none(),
2267 "Dust isolation should NOT be active",
2268 );
2269
2270 assert_eq!(user_builder.outputs.len(), 2);
2272
2273 assert_eq!(user_builder.nb_sigs(), 3);
2275
2276 let user_builder = user_builder.generate_user_nonces(alice_keypair);
2278 let cosign_request = user_builder.cosign_request();
2279
2280 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
2282 .expect("Invalid cosign request")
2283 .server_cosign(&server_keypair)
2284 .expect("Incorrect key");
2285
2286 let cosign_data = server_builder.cosign_response();
2287
2288 let signed_builder = user_builder
2290 .user_cosign(&alice_keypair, &cosign_data)
2291 .expect("Valid cosign data and correct key");
2292 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
2293 let vtxos = signed_builder.build_signed_vtxos();
2294
2295 assert_eq!(vtxos.len(), 2);
2297
2298 for vtxo in vtxos.into_iter() {
2299 vtxo.validate(&funding_tx).expect("Invalid VTXO");
2301
2302 assert_eq!(vtxo.amount(), Amount::from_sat(100));
2304
2305 let mut prev_tx = funding_tx.clone();
2307 for tx in vtxo.transactions().map(|item| item.tx) {
2308 let prev_outpoint: OutPoint = tx.input[0].previous_output;
2309 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
2310 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
2311 prev_tx = tx;
2312 }
2313 }
2314 }
2315
2316 #[test]
2317 fn spend_nondust_vtxo_to_dust() {
2318 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2321 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2322 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2323
2324 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2326 amount: Amount::from_sat(500),
2327 fee: Amount::ZERO,
2328 expiry_height: BlockHeight::new(1000),
2329 exit_delta: BlockDelta::new(128),
2330 user_keypair: alice_keypair.clone(),
2331 server_keypair: server_keypair.clone()
2332 }.build();
2333
2334 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
2335
2336 let dust_outputs = vec![
2339 ArkoorDestination {
2340 total_amount: Amount::from_sat(250),
2341 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2342 },
2343 ArkoorDestination {
2344 total_amount: Amount::from_sat(250),
2345 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
2346 }
2347 ];
2348
2349 let user_builder = ArkoorBuilder::new_with_checkpoint(
2350 alice_vtxo.clone(),
2351 dust_outputs,
2352 vec![],
2353 ).expect("Valid arkoor request for non-dust to dust case");
2354
2355 assert!(
2357 user_builder.unsigned_isolation_fanout_tx.is_none(),
2358 "Dust isolation should NOT be active",
2359 );
2360
2361 assert_eq!(user_builder.outputs.len(), 2);
2363
2364 assert_eq!(user_builder.nb_sigs(), 3);
2366
2367 let user_builder = user_builder.generate_user_nonces(alice_keypair);
2369 let cosign_request = user_builder.cosign_request();
2370
2371 let server_builder = ArkoorBuilder::from_cosign_request(cosign_request)
2373 .expect("Invalid cosign request")
2374 .server_cosign(&server_keypair)
2375 .expect("Incorrect key");
2376
2377 let cosign_data = server_builder.cosign_response();
2378
2379 let signed_builder = user_builder
2381 .user_cosign(&alice_keypair, &cosign_data)
2382 .expect("Valid cosign data and correct key");
2383 verify_signed_internal_vtxos(&signed_builder, &funding_tx);
2384 let vtxos = signed_builder.build_signed_vtxos();
2385
2386 assert_eq!(vtxos.len(), 2);
2388
2389 for vtxo in vtxos.into_iter() {
2390 vtxo.validate(&funding_tx).expect("Invalid VTXO");
2392
2393 assert_eq!(vtxo.amount(), Amount::from_sat(250));
2395
2396 let mut prev_tx = funding_tx.clone();
2398 for tx in vtxo.transactions().map(|item| item.tx) {
2399 let prev_outpoint: OutPoint = tx.input[0].previous_output;
2400 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
2401 crate::test_util::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
2402 prev_tx = tx;
2403 }
2404 }
2405 }
2406
2407 #[test]
2408 fn isolate_dust_all_nondust() {
2409 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2412 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2413 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2414
2415 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2416 amount: Amount::from_sat(1000),
2417 fee: Amount::ZERO,
2418 expiry_height: BlockHeight::new(1000),
2419 exit_delta: BlockDelta::new(128),
2420 user_keypair: alice_keypair.clone(),
2421 server_keypair: server_keypair.clone()
2422 }.build();
2423
2424 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2425
2426 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2427 alice_vtxo,
2428 vec![
2429 ArkoorDestination {
2430 total_amount: Amount::from_sat(500),
2431 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2432 },
2433 ArkoorDestination {
2434 total_amount: Amount::from_sat(500),
2435 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2436 }
2437 ],
2438 ).unwrap();
2439
2440 assert!(builder.unsigned_isolation_fanout_tx.is_none());
2442
2443 assert_eq!(builder.outputs.len(), 2);
2445 assert_eq!(builder.isolated_outputs.len(), 0);
2446 }
2447
2448 #[test]
2449 fn isolate_dust_all_dust() {
2450 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2453 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2454 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2455
2456 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2457 amount: Amount::from_sat(400),
2458 fee: Amount::ZERO,
2459 expiry_height: BlockHeight::new(1000),
2460 exit_delta: BlockDelta::new(128),
2461 user_keypair: alice_keypair.clone(),
2462 server_keypair: server_keypair.clone()
2463 }.build();
2464
2465 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2466
2467 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2468 alice_vtxo,
2469 vec![
2470 ArkoorDestination {
2471 total_amount: Amount::from_sat(200),
2472 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2473 },
2474 ArkoorDestination {
2475 total_amount: Amount::from_sat(200),
2476 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2477 }
2478 ],
2479 ).unwrap();
2480
2481 assert!(builder.unsigned_isolation_fanout_tx.is_none());
2483
2484 assert_eq!(builder.outputs.len(), 2);
2486 assert_eq!(builder.isolated_outputs.len(), 0);
2487 }
2488
2489 #[test]
2490 fn isolate_dust_sufficient_dust() {
2491 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2494 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2495 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2496
2497 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2498 amount: Amount::from_sat(1000),
2499 fee: Amount::ZERO,
2500 expiry_height: BlockHeight::new(1000),
2501 exit_delta: BlockDelta::new(128),
2502 user_keypair: alice_keypair.clone(),
2503 server_keypair: server_keypair.clone()
2504 }.build();
2505
2506 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2507
2508 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2510 alice_vtxo,
2511 vec![
2512 ArkoorDestination {
2513 total_amount: Amount::from_sat(600),
2514 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2515 },
2516 ArkoorDestination {
2517 total_amount: Amount::from_sat(200),
2518 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2519 },
2520 ArkoorDestination {
2521 total_amount: Amount::from_sat(200),
2522 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2523 }
2524 ],
2525 ).unwrap();
2526
2527 assert!(builder.unsigned_isolation_fanout_tx.is_some());
2529
2530 assert_eq!(builder.outputs.len(), 1);
2532 assert_eq!(builder.isolated_outputs.len(), 2);
2533 }
2534
2535 #[test]
2536 fn isolate_dust_split_successful() {
2537 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2541 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2542 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2543
2544 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2545 amount: Amount::from_sat(1000),
2546 fee: Amount::ZERO,
2547 expiry_height: BlockHeight::new(1000),
2548 exit_delta: BlockDelta::new(128),
2549 user_keypair: alice_keypair.clone(),
2550 server_keypair: server_keypair.clone()
2551 }.build();
2552
2553 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2554
2555 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2556 alice_vtxo,
2557 vec![
2558 ArkoorDestination {
2559 total_amount: Amount::from_sat(800),
2560 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2561 },
2562 ArkoorDestination {
2563 total_amount: Amount::from_sat(100),
2564 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2565 },
2566 ArkoorDestination {
2567 total_amount: Amount::from_sat(100),
2568 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2569 }
2570 ],
2571 ).unwrap();
2572
2573 assert!(builder.unsigned_isolation_fanout_tx.is_some());
2575
2576 assert_eq!(builder.outputs.len(), 1);
2578 assert_eq!(builder.isolated_outputs.len(), 3);
2579
2580 assert_eq!(builder.outputs[0].total_amount, Amount::from_sat(670));
2582 let isolated_sum: Amount = builder.isolated_outputs.iter().map(|o| o.total_amount).sum();
2583 assert_eq!(isolated_sum, P2TR_DUST);
2584 }
2585
2586 #[test]
2587 fn isolate_dust_split_impossible() {
2588 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2593 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2594 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2595
2596 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2597 amount: Amount::from_sat(600),
2598 fee: Amount::ZERO,
2599 expiry_height: BlockHeight::new(1000),
2600 exit_delta: BlockDelta::new(128),
2601 user_keypair: alice_keypair.clone(),
2602 server_keypair: server_keypair.clone()
2603 }.build();
2604
2605 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2606
2607 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2608 alice_vtxo,
2609 vec![
2610 ArkoorDestination {
2611 total_amount: Amount::from_sat(400),
2612 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2613 },
2614 ArkoorDestination {
2615 total_amount: Amount::from_sat(100),
2616 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2617 },
2618 ArkoorDestination {
2619 total_amount: Amount::from_sat(100),
2620 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2621 }
2622 ],
2623 ).unwrap();
2624
2625 assert!(builder.unsigned_isolation_fanout_tx.is_none());
2627
2628 assert_eq!(builder.outputs.len(), 3);
2630 assert_eq!(builder.isolated_outputs.len(), 0);
2631 }
2632
2633 #[test]
2634 fn isolate_dust_exactly_boundary() {
2635 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2639 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2640 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2641
2642 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2643 amount: Amount::from_sat(1000),
2644 fee: Amount::ZERO,
2645 expiry_height: BlockHeight::new(1000),
2646 exit_delta: BlockDelta::new(128),
2647 user_keypair: alice_keypair.clone(),
2648 server_keypair: server_keypair.clone()
2649 }.build();
2650
2651 alice_vtxo.validate(&funding_tx).expect("Valid vtxo");
2652
2653 let builder = ArkoorBuilder::new_with_checkpoint_isolate_dust(
2654 alice_vtxo,
2655 vec![
2656 ArkoorDestination {
2657 total_amount: Amount::from_sat(660),
2658 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2659 },
2660 ArkoorDestination {
2661 total_amount: Amount::from_sat(170),
2662 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2663 },
2664 ArkoorDestination {
2665 total_amount: Amount::from_sat(170),
2666 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
2667 }
2668 ],
2669 ).unwrap();
2670
2671 assert!(builder.unsigned_isolation_fanout_tx.is_some());
2673
2674 assert_eq!(builder.outputs.len(), 1);
2676 assert_eq!(builder.isolated_outputs.len(), 2);
2677
2678 assert_eq!(builder.outputs[0].total_amount, Amount::from_sat(660));
2680 assert_eq!(builder.isolated_outputs[0].total_amount, Amount::from_sat(170));
2681 assert_eq!(builder.isolated_outputs[1].total_amount, Amount::from_sat(170));
2682 }
2683
2684 #[test]
2685 fn validate_amounts_output_sum_overflow_rejected() {
2686 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2691 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2692 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2693
2694 let (_funding_tx, alice_vtxo) = DummyTestVtxoSpec {
2695 amount: Amount::from_sat(10_330),
2696 fee: Amount::from_sat(330),
2697 expiry_height: BlockHeight::new(1000),
2698 exit_delta: BlockDelta::new(128),
2699 user_keypair: alice_keypair,
2700 server_keypair,
2701 }.build();
2702
2703 let outputs = vec![
2704 ArkoorDestination {
2705 total_amount: Amount::from_sat(u64::MAX),
2706 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key()),
2707 },
2708 ArkoorDestination {
2709 total_amount: Amount::from_sat(u64::MAX),
2710 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key()),
2711 },
2712 ];
2713
2714 let result = ArkoorBuilder::new_with_checkpoint(alice_vtxo, outputs, vec![]);
2715 assert_eq!(result.err(), Some(ArkoorConstructionError::Overflow));
2716 }
2717
2718 fn verify_isolation_rules(
2723 normal_amounts: &[u64],
2724 isolation_amounts: &[u64],
2725 ) -> Result<(), ArkoorConstructionError> {
2726 let user_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2727 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2728
2729 let total = normal_amounts.iter().chain(isolation_amounts).sum::<u64>();
2731 let (_funding_tx, vtxo) = DummyTestVtxoSpec {
2732 amount: Amount::from_sat(total.max(1)),
2733 fee: Amount::ZERO,
2734 expiry_height: BlockHeight::new(1000),
2735 exit_delta: BlockDelta::new(128),
2736 user_keypair: user_keypair.clone(),
2737 server_keypair,
2738 }.build();
2739
2740 let dest = |amount: &u64| ArkoorDestination {
2741 total_amount: Amount::from_sat(*amount),
2742 policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
2743 };
2744 let outputs = normal_amounts.iter().map(dest).collect::<Vec<_>>();
2745 let isolated_outputs = isolation_amounts.iter().map(dest).collect::<Vec<_>>();
2746
2747 ArkoorBuilder::new_with_checkpoint(vtxo, outputs, isolated_outputs).map(|_| ())
2748 }
2749
2750 #[test]
2751 fn output_placement_rules() {
2752 verify_isolation_rules(&[], &[]).expect_err("no outputs");
2753 verify_isolation_rules(&[100], &[]).expect("all-dust spend stays in normal outputs");
2754 verify_isolation_rules(&[200, 200], &[])
2755 .expect("no dust-isolation tx needed when everything is dust");
2756 verify_isolation_rules(&[200, 400], &[]).expect("dust may mix with sub-660 outputs");
2757 verify_isolation_rules(&[200, 500], &[]).expect("dust may mix with small outputs");
2758 verify_isolation_rules(&[600, 200, 200], &[])
2762 .expect("dust may mix when no output reaches 660");
2763 verify_isolation_rules(&[600], &[200, 200])
2764 .expect("the same outputs may also use isolation");
2765 verify_isolation_rules(&[], &[100, 200])
2766 .expect_err("no spurious dust-isolation without normal outputs");
2767 verify_isolation_rules(&[500], &[100])
2768 .expect_err("isolation sum below the dust limit next to normal outputs");
2769 verify_isolation_rules(&[10_000], &[100, 100])
2770 .expect_err("isolation sum must reach the dust limit");
2771 verify_isolation_rules(&[500], &[200, 200]).expect("isolation sum reaches the dust limit");
2772 verify_isolation_rules(&[], &[660]).expect_err("no normal outputs");
2773 verify_isolation_rules(&[], &[659]).expect_err("no normal outputs");
2774 verify_isolation_rules(&[400], &[660])
2775 .expect_err("660 sat can be split and does not need isolation");
2776 verify_isolation_rules(&[400], &[659])
2777 .expect_err("dust-isolation must contain at least one dust output");
2778 verify_isolation_rules(&[400], &[659, 100]).expect("just below the split threshold");
2779 verify_isolation_rules(&[330], &[]).expect("the dust limit itself is a valid normal output");
2780 verify_isolation_rules(&[10_000], &[100_000])
2781 .expect_err("large outputs never need isolation");
2782 verify_isolation_rules(&[10_000], &[100, 10_000])
2783 .expect_err("isolated outputs cannot be large");
2784 verify_isolation_rules(&[200], &[400, 100])
2785 .expect_err("dust in the normal outputs while dust-isolation is used");
2786 verify_isolation_rules(&[100, 10_000], &[])
2787 .expect_err("dust must not share the checkpoint with a large output");
2788
2789 assert_eq!(
2791 verify_isolation_rules(&[200, 10_000], &[]).unwrap_err(),
2792 ArkoorConstructionError::Dust,
2793 "dust must not share the checkpoint with a large output",
2794 );
2795 }
2796
2797 #[test]
2798 #[ignore = "slow; run on demand with --run-ignored all"]
2799 fn isolate_dust_fuzz() {
2800 let mut rng = rand::rngs::StdRng::seed_from_u64(1105);
2804
2805 let user_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2806 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
2807
2808 for nb_outputs in 1..=4 {
2809 for _ in 0..2_500 {
2810 let amounts = (0..nb_outputs)
2811 .map(|_| rng.gen_range(1..10_000u64))
2812 .collect::<Vec<u64>>();
2813
2814 let (_funding_tx, vtxo) = DummyTestVtxoSpec {
2815 amount: Amount::from_sat(amounts.iter().sum()),
2816 fee: Amount::ZERO,
2817 expiry_height: BlockHeight::new(1000),
2818 exit_delta: BlockDelta::new(128),
2819 user_keypair: user_keypair.clone(),
2820 server_keypair: server_keypair.clone(),
2821 }.build();
2822
2823 let outputs = amounts.iter().map(|a| ArkoorDestination {
2824 total_amount: Amount::from_sat(*a),
2825 policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
2826 }).collect::<Vec<_>>();
2827
2828 let result = ArkoorBuilder::new_with_checkpoint_isolate_dust(vtxo, outputs);
2829 assert!(result.is_ok(), "amounts {:?}: {:?}", amounts, result.err());
2830 }
2831 }
2832 }
2833}