1
2use std::convert::Infallible;
3
4use bitcoin::{Transaction, Txid};
5use bitcoin::secp256k1::Keypair;
6
7use crate::{Vtxo, VtxoId, VtxoPolicy, ServerVtxo, Amount};
8use crate::arkoor::ArkoorDestination;
9use crate::arkoor::{
10 ArkoorBuilder, ArkoorConstructionError, state, ArkoorCosignResponse,
11 ArkoorSigningError, ArkoorCosignRequest,
12};
13use crate::vtxo::Full;
14
15
16pub struct ArkoorPackageBuilder<S: state::BuilderState> {
24 pub builders: Vec<ArkoorBuilder<S>>,
25}
26
27#[derive(Debug, Clone)]
28pub struct ArkoorPackageCosignRequest<V> {
29 pub requests: Vec<ArkoorCosignRequest<V>>
30}
31
32impl<V> ArkoorPackageCosignRequest<V> {
33 pub fn convert_vtxo<F, O>(self, mut f: F) -> ArkoorPackageCosignRequest<O>
34 where F: FnMut(V) -> O
35 {
36 ArkoorPackageCosignRequest {
37 requests: self.requests.into_iter().map(|r| {
38 ArkoorCosignRequest {
39 user_pub_nonces: r.user_pub_nonces,
40 input: f(r.input),
41 outputs: r.outputs,
42 isolated_outputs: r.isolated_outputs,
43 use_checkpoint: r.use_checkpoint,
44 attestation: r.attestation,
45 }
46 }).collect::<Vec<_>>(),
47 }
48 }
49
50 pub fn inputs(&self) -> impl Iterator<Item=&V> {
51 self.requests.iter()
52 .map(|r| Some(&r.input))
53 .flatten()
54 }
55
56 pub fn all_outputs(
57 &self,
58 ) -> impl Iterator<Item = &ArkoorDestination> + Clone {
59 self.requests.iter()
60 .map(|r| r.all_outputs())
61 .flatten()
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
66#[error("VTXO id mismatch. Expected {expected}, got {got}")]
67pub struct InputMismatchError {
68 expected: VtxoId,
69 got: VtxoId,
70}
71
72impl ArkoorPackageCosignRequest<VtxoId> {
73 pub fn set_vtxos(
74 self,
75 vtxos: impl IntoIterator<Item = Vtxo<Full>>,
76 ) -> Result<ArkoorPackageCosignRequest<Vtxo<Full>>, InputMismatchError> {
77 let package = ArkoorPackageCosignRequest {
78 requests: self.requests.into_iter().zip(vtxos).map(|(r, vtxo)| {
79 if r.input != vtxo.id() {
80 return Err(InputMismatchError {
81 expected: r.input,
82 got: vtxo.id(),
83 })
84 }
85
86 Ok(ArkoorCosignRequest {
87 input: vtxo,
88 user_pub_nonces: r.user_pub_nonces,
89 outputs: r.outputs,
90 isolated_outputs: r.isolated_outputs,
91 use_checkpoint: r.use_checkpoint,
92 attestation: r.attestation,
93 })
94 }).collect::<Result<Vec<_>, _>>()?,
95 };
96
97 Ok(package)
98 }
99}
100
101#[derive(Debug, Clone)]
102pub struct ArkoorPackageCosignResponse {
103 pub responses: Vec<ArkoorCosignResponse>
104}
105
106impl ArkoorPackageBuilder<state::Initial> {
107 fn allocate_outputs_to_inputs(
112 inputs: impl IntoIterator<Item = Vtxo<Full>>,
113 outputs: Vec<ArkoorDestination>,
114 ) -> Result<Vec<(Vtxo<Full>, Vec<ArkoorDestination>)>, ArkoorConstructionError> {
115 let total_output = outputs.iter().map(|r| r.total_amount).sum::<Amount>();
116 if outputs.is_empty() || total_output == Amount::ZERO {
117 return Err(ArkoorConstructionError::NoOutputs);
118 }
119
120 let mut allocations: Vec<(Vtxo<Full>, Vec<ArkoorDestination>)> = Vec::new();
121
122 let mut output_iter = outputs.into_iter();
123 let mut current_output = output_iter.next();
124 let mut current_output_remaining = current_output.as_ref()
125 .map(|o| o.total_amount).unwrap_or_default();
126
127 let mut total_input = Amount::ZERO;
128 'inputs:
129 for input in inputs {
130 total_input += input.amount();
131
132 let mut input_remaining = input.amount();
133 let mut input_allocation: Vec<ArkoorDestination> = Vec::new();
134
135 'outputs:
136 while let Some(ref output) = current_output {
137 let _: Infallible = if input_remaining == current_output_remaining {
138 input_allocation.push(ArkoorDestination {
140 total_amount: current_output_remaining,
141 policy: output.policy.clone(),
142 });
143
144 current_output = output_iter.next();
145 current_output_remaining = current_output.as_ref()
146 .map(|o| o.total_amount).unwrap_or_default();
147 allocations.push((input, input_allocation));
148 continue 'inputs;
149 } else if input_remaining > current_output_remaining {
150 input_allocation.push(ArkoorDestination {
152 total_amount: current_output_remaining,
153 policy: output.policy.clone(),
154 });
155
156 input_remaining -= current_output_remaining;
157
158 current_output = output_iter.next();
159 current_output_remaining = current_output.as_ref()
160 .map(|o| o.total_amount).unwrap_or_default();
161 continue 'outputs;
162 } else {
163 input_allocation.push(ArkoorDestination {
165 total_amount: input_remaining,
166 policy: output.policy.clone(),
167 });
168
169 current_output_remaining -= input_remaining;
170
171 allocations.push((input, input_allocation));
172 continue 'inputs;
173 };
174 }
175 }
176
177 if total_input != total_output {
178 return Err(ArkoorConstructionError::Unbalanced {
179 input: total_input,
180 output: total_output,
181 });
182 }
183
184 Ok(allocations)
185 }
186
187 pub fn new_with_checkpoints(
189 inputs: impl IntoIterator<Item = Vtxo<Full>>,
190 outputs: Vec<ArkoorDestination>,
191 ) -> Result<Self, ArkoorConstructionError> {
192 Self::new(inputs, outputs, true)
193 }
194
195 pub fn new_without_checkpoints(
197 inputs: impl IntoIterator<Item = Vtxo<Full>>,
198 outputs: Vec<ArkoorDestination>,
199 ) -> Result<Self, ArkoorConstructionError> {
200 Self::new(inputs, outputs, false)
201 }
202
203 pub fn new_single_output_with_checkpoints(
208 inputs: impl IntoIterator<Item = Vtxo<Full>>,
209 output: ArkoorDestination,
210 change_policy: VtxoPolicy,
211 ) -> Result<Self, ArkoorConstructionError> {
212 let inputs = inputs.into_iter().collect::<Vec<_>>();
214 let total_input = inputs.iter().map(|v| v.amount()).sum::<Amount>();
215
216 let change_amount = total_input.checked_sub(output.total_amount)
217 .ok_or(ArkoorConstructionError::Unbalanced {
218 input: total_input,
219 output: output.total_amount,
220 })?;
221
222 let outputs = if change_amount == Amount::ZERO {
223 vec![output]
224 } else {
225 vec![
226 output,
227 ArkoorDestination {
228 total_amount: change_amount,
229 policy: change_policy,
230 },
231 ]
232 };
233
234 Self::new_with_checkpoints(inputs, outputs)
235 }
236
237 pub fn new_claim_all_with_checkpoints(
239 inputs: impl IntoIterator<Item = Vtxo<Full>>,
240 output_policy: VtxoPolicy,
241 ) -> Result<Self, ArkoorConstructionError> {
242 let inputs = inputs.into_iter().collect::<Vec<_>>();
244 let total_input = inputs.iter().map(|v| v.amount()).sum::<Amount>();
245
246 let output = ArkoorDestination {
247 total_amount: total_input,
248 policy: output_policy,
249 };
250
251 Self::new_with_checkpoints(inputs, vec![output])
252 }
253
254 pub fn new_claim_all_without_checkpoints(
256 inputs: impl IntoIterator<Item = Vtxo<Full>>,
257 output_policy: VtxoPolicy,
258 ) -> Result<Self, ArkoorConstructionError> {
259 let inputs = inputs.into_iter().collect::<Vec<_>>();
261 let total_input = inputs.iter().map(|v| v.amount()).sum::<Amount>();
262
263 let output = ArkoorDestination {
264 total_amount: total_input,
265 policy: output_policy,
266 };
267
268 Self::new_without_checkpoints(inputs, vec![output])
269 }
270
271 fn new(
272 inputs: impl IntoIterator<Item = Vtxo<Full>>,
273 outputs: Vec<ArkoorDestination>,
274 use_checkpoint: bool,
275 ) -> Result<Self, ArkoorConstructionError> {
276 let allocations = Self::allocate_outputs_to_inputs(inputs, outputs)?;
278
279 let mut builders = Vec::with_capacity(allocations.len());
281 for (input, allocated_outputs) in allocations {
282 let builder = ArkoorBuilder::new_isolate_dust(
283 input,
284 allocated_outputs,
285 use_checkpoint,
286 )?;
287 builders.push(builder);
288 }
289
290 Ok(Self { builders })
291 }
292
293 pub fn generate_user_nonces(
294 self,
295 user_keypairs: &[Keypair],
296 ) -> Result<ArkoorPackageBuilder<state::UserGeneratedNonces>, ArkoorSigningError> {
297 if user_keypairs.len() != self.builders.len() {
298 return Err(ArkoorSigningError::InvalidNbKeypairs {
299 expected: self.builders.len(),
300 got: user_keypairs.len(),
301 })
302 }
303
304 let mut builder = Vec::with_capacity(self.builders.len());
305 for (idx, package) in self.builders.into_iter().enumerate() {
306 builder.push(package.generate_user_nonces(user_keypairs[idx]));
307 }
308 Ok(ArkoorPackageBuilder { builders: builder })
309 }
310
311 pub fn cosign_both(
315 self,
316 user_keypairs: &[Keypair],
317 server_keypair: &Keypair,
318 ) -> Result<ArkoorPackageBuilder<state::UserSigned>, ArkoorSigningError> {
319 if user_keypairs.len() != self.builders.len() {
320 return Err(ArkoorSigningError::InvalidNbKeypairs {
321 expected: self.builders.len(),
322 got: user_keypairs.len(),
323 })
324 }
325
326 let mut packages = Vec::with_capacity(self.builders.len());
327 for (idx, pkg) in self.builders.into_iter().enumerate() {
328 packages.push(pkg.cosign_both(&user_keypairs[idx], server_keypair)?);
329 }
330 Ok(ArkoorPackageBuilder { builders: packages })
331 }
332}
333
334impl ArkoorPackageBuilder<state::UserGeneratedNonces> {
335 pub fn user_cosign(
336 self,
337 user_keypairs: &[Keypair],
338 server_cosign_response: ArkoorPackageCosignResponse,
339 ) -> Result<ArkoorPackageBuilder<state::UserSigned>, ArkoorSigningError> {
340 if server_cosign_response.responses.len() != self.builders.len() {
341 return Err(ArkoorSigningError::InvalidNbPackages {
342 expected: self.builders.len(),
343 got: server_cosign_response.responses.len()
344 })
345 }
346
347 if user_keypairs.len() != self.builders.len() {
348 return Err(ArkoorSigningError::InvalidNbKeypairs {
349 expected: self.builders.len(),
350 got: user_keypairs.len(),
351 })
352 }
353
354 let mut packages = Vec::with_capacity(self.builders.len());
355
356 for (idx, pkg) in self.builders.into_iter().enumerate() {
357 packages.push(pkg.user_cosign(
358 &user_keypairs[idx],
359 &server_cosign_response.responses[idx],
360 )?,);
361 }
362 Ok(ArkoorPackageBuilder { builders: packages })
363 }
364
365 pub fn cosign_request(&self) -> ArkoorPackageCosignRequest<Vtxo<Full>> {
366 let requests = self.builders.iter()
367 .map(|package| package.cosign_request())
368 .collect::<Vec<_>>();
369
370 ArkoorPackageCosignRequest { requests }
371 }
372
373}
374
375impl ArkoorPackageBuilder<state::UserSigned> {
376 pub fn build_signed_vtxos(self) -> Vec<Vtxo<Full>> {
377 self.builders.into_iter()
378 .map(|b| b.build_signed_vtxos())
379 .flatten()
380 .collect::<Vec<_>>()
381 }
382
383 pub fn build_signed_internal_vtxos(&self) -> Vec<(ServerVtxo<Full>, Txid)> {
386 self.builders.iter()
387 .map(|b| b.build_signed_internal_vtxos())
388 .flatten()
389 .collect()
390 }
391
392 pub fn signed_virtual_transactions(&self) -> Vec<Transaction> {
393 self.builders.iter()
394 .flat_map(|b| b.signed_virtual_transactions())
395 .collect()
396 }
397}
398
399impl ArkoorPackageBuilder<state::ServerCanCosign> {
400 pub fn from_cosign_request(
401 cosign_request: ArkoorPackageCosignRequest<Vtxo<Full>>,
402 ) -> Result<Self, (usize, ArkoorSigningError)> {
403 let request_iter = cosign_request.requests.into_iter();
404 let mut packages = Vec::with_capacity(request_iter.size_hint().0);
405 for (idx, request) in request_iter.enumerate() {
406 packages.push(ArkoorBuilder::from_cosign_request(request)
407 .map_err(|e| (idx, e))?);
408 }
409
410 Ok(Self { builders: packages })
411 }
412
413 pub fn server_cosign(
414 self,
415 server_keypair: &Keypair,
416 ) -> Result<ArkoorPackageBuilder<state::ServerSigned>, ArkoorSigningError> {
417 let mut packages = Vec::with_capacity(self.builders.len());
418 for package in self.builders.into_iter() {
419 packages.push(package.server_cosign(&server_keypair)?);
420 }
421 Ok(ArkoorPackageBuilder { builders: packages })
422 }
423}
424
425impl ArkoorPackageBuilder<state::ServerSigned> {
426 pub fn cosign_response(&self) -> ArkoorPackageCosignResponse {
427 let responses = self.builders.iter()
428 .map(|package| package.cosign_response())
429 .collect::<Vec<_>>();
430
431 ArkoorPackageCosignResponse { responses }
432 }
433}
434
435impl<S: state::BuilderState> ArkoorPackageBuilder<S> {
436 pub fn input_ids<'a>(&'a self) -> impl Iterator<Item = VtxoId> + Clone + 'a {
438 self.builders.iter().map(|b| b.input().id())
439 }
440
441 pub fn build_unsigned_vtxos<'a>(&'a self) -> impl Iterator<Item = Vtxo<Full>> + 'a {
442 self.builders.iter()
443 .map(|b| b.build_unsigned_vtxos())
444 .flatten()
445 }
446
447 pub fn build_unsigned_internal_vtxos(&self) -> Vec<(ServerVtxo<Full>, Txid)> {
450 self.builders.iter()
451 .map(|b| b.build_unsigned_internal_vtxos())
452 .flatten()
453 .collect()
454 }
455
456 pub fn input_spend_info<'a>(&'a self) -> impl Iterator<Item = (VtxoId, Txid)> + 'a {
458 self.builders.iter().map(|b| b.input_spend_info())
459 }
460
461 pub fn spend_info<'a>(&'a self) -> impl Iterator<Item = (VtxoId, Txid)> + 'a {
464 self.builders.iter()
465 .map(|b| b.spend_info())
466 .flatten()
467 }
468
469 pub fn virtual_transactions<'a>(&'a self) -> impl Iterator<Item = Txid> + 'a {
470 self.builders.iter()
471 .flat_map(|b| b.virtual_transactions())
472 }
473}
474
475#[cfg(test)]
476mod test {
477 use std::collections::{HashMap, HashSet};
478 use std::str::FromStr;
479
480 use bitcoin::{Transaction, Txid};
481 use bitcoin::secp256k1::Keypair;
482
483 use bitcoin_ext::P2TR_DUST;
484
485 use super::*;
486 use crate::test_util::dummy::DummyTestVtxoSpec;
487 use crate::PublicKey;
488
489 fn server_keypair() -> Keypair {
490 Keypair::from_str("f7a2a5d150afb575e98fff9caeebf6fbebbaeacfdfa7433307b208b39f1155f2").expect("Invalid key")
491 }
492
493 fn alice_keypair() -> Keypair {
494 Keypair::from_str("9b4382c8985f12e4bd8d1b51e63615bf0187843630829f4c5e9c45ef2cf994a4").expect("Invalid key")
495 }
496
497 fn bob_keypair() -> Keypair {
498 Keypair::from_str("c86435ba7e30d7afd7c5df9f3263ce2eb86b3ff9866a16ccd22a0260496ddf0f").expect("Invalid key")
499 }
500
501
502 fn alice_public_key() -> PublicKey {
503 alice_keypair().public_key()
504 }
505
506 fn bob_public_key() -> PublicKey {
507 bob_keypair().public_key()
508 }
509
510 fn dummy_vtxo_for_amount(amt: Amount) -> (Transaction, Vtxo<Full>) {
511 DummyTestVtxoSpec {
512 amount: amt + P2TR_DUST,
513 fee: P2TR_DUST,
514 expiry_height: 1000,
515 exit_delta: 128,
516 user_keypair: alice_keypair(),
517 server_keypair: server_keypair()
518 }.build()
519 }
520
521 fn verify_package_builder(
522 builder: ArkoorPackageBuilder<state::Initial>,
523 keypairs: &[Keypair],
524 funding_tx_map: HashMap<Txid, Transaction>,
525 ) {
526 let vtxs: Vec<Txid> = builder.virtual_transactions().collect();
528 let vtx_set: HashSet<Txid> = vtxs.iter().copied().collect();
529 let spend_txids: HashSet<Txid> = builder.spend_info().map(|(_, txid)| txid).collect();
530
531 assert_eq!(vtxs.len(), vtx_set.len(), "virtual_transactions() contains duplicates");
533
534 for txid in &vtx_set {
536 assert!(spend_txids.contains(txid), "virtual_transaction {} not in spend_info", txid);
537 }
538
539 for txid in &spend_txids {
541 assert!(vtx_set.contains(txid), "spend_info txid {} not in virtual_transactions", txid);
542 }
543
544 let user_builder = builder.generate_user_nonces(keypairs).expect("Valid nb of keypairs");
545 let cosign_requests = user_builder.cosign_request();
546
547 let cosign_responses = ArkoorPackageBuilder::from_cosign_request(cosign_requests)
548 .expect("Invalid cosign requests")
549 .server_cosign(&server_keypair())
550 .expect("Wrong server key")
551 .cosign_response();
552
553
554 let vtxos = user_builder.user_cosign(keypairs, cosign_responses)
555 .expect("Invalid cosign responses")
556 .build_signed_vtxos();
557
558 for vtxo in vtxos {
559 let funding_txid = vtxo.chain_anchor().txid;
560 let funding_tx = funding_tx_map.get(&funding_txid).expect("Funding tx not found");
561 vtxo.validate(&funding_tx).expect("Invalid vtxo");
562
563 let mut prev_tx = funding_tx.clone();
564 for tx in vtxo.transactions().map(|item| item.tx) {
565 crate::test_util::verify_tx(
566 &[prev_tx.output[vtxo.chain_anchor().vout as usize].clone()],
567 0,
568 &tx).expect("Invalid transaction");
569 prev_tx = tx;
570 }
571 }
572 }
573
574 #[test]
575 fn send_full_vtxo() {
576 let (funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(100_000));
579
580 let package_builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
581 [alice_vtxo],
582 ArkoorDestination {
583 total_amount: Amount::from_sat(100_000),
584 policy: VtxoPolicy::new_pubkey(bob_public_key()),
585 },
586 VtxoPolicy::new_pubkey(alice_public_key())
587 ).expect("Valid package");
588
589 let funding_map = HashMap::from([(funding_tx.compute_txid(), funding_tx)]);
590 verify_package_builder(package_builder, &[alice_keypair()], funding_map);
591 }
592
593 #[test]
594 fn arkoor_dust_change() {
595 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
599 let package_builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
600 [alice_vtxo],
601 ArkoorDestination {
602 total_amount: Amount::from_sat(900),
603 policy: VtxoPolicy::new_pubkey(bob_public_key()),
604 },
605 VtxoPolicy::new_pubkey(alice_public_key())
606 ).expect("Valid package");
607
608 let vtxos: Vec<Vtxo<Full>> = package_builder.build_unsigned_vtxos().collect();
610 assert_eq!(vtxos.len(), 3);
611 assert_eq!(vtxos[0].amount(), Amount::from_sat(670));
612 assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
613 assert_eq!(vtxos[1].amount(), Amount::from_sat(230));
614 assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
615 assert_eq!(vtxos[2].amount(), Amount::from_sat(100));
616 assert_eq!(vtxos[2].policy().user_pubkey(), alice_public_key());
617 }
618
619 #[test]
620 fn can_send_multiple_inputs() {
621 let (funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
624 let (funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
625 let (funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
626
627 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
628 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
629 ArkoorDestination {
630 total_amount: Amount::from_sat(17_000),
631 policy: VtxoPolicy::new_pubkey(bob_public_key()),
632 },
633 VtxoPolicy::new_pubkey(alice_public_key())
634 ).expect("Valid package");
635
636 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
637 assert_eq!(vtxos.len(), 3);
638 assert_eq!(vtxos[0].amount(), Amount::from_sat(10_000));
639 assert_eq!(vtxos[1].amount(), Amount::from_sat(5_000));
640 assert_eq!(vtxos[2].amount(), Amount::from_sat(2_000));
641 assert_eq!(
642 vtxos.iter().map(|v| v.policy().user_pubkey()).collect::<Vec<_>>(),
643 vec![bob_public_key(); 3],
644 );
645
646 let funding_map = HashMap::from([
647 (funding_tx_1.compute_txid(), funding_tx_1),
648 (funding_tx_2.compute_txid(), funding_tx_2),
649 (funding_tx_3.compute_txid(), funding_tx_3),
650 ]);
651 verify_package_builder(
652 package, &[alice_keypair(), alice_keypair(), alice_keypair()], funding_map,
653 );
654 }
655
656 #[test]
657 fn can_send_multiple_inputs_with_change() {
658 let (funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
662 let (funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
663 let (funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
664
665 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
666 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
667 ArkoorDestination {
668 total_amount: Amount::from_sat(16_000),
669 policy: VtxoPolicy::new_pubkey(bob_public_key()),
670 },
671 VtxoPolicy::new_pubkey(alice_public_key())
672 ).expect("Valid package");
673
674 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
675 assert_eq!(vtxos.len(), 4);
676 assert_eq!(vtxos[0].amount(), Amount::from_sat(10_000));
677 assert_eq!(vtxos[1].amount(), Amount::from_sat(5_000));
678 assert_eq!(vtxos[2].amount(), Amount::from_sat(1_000));
679 assert_eq!(vtxos[3].amount(), Amount::from_sat(1_000),
680 "Alice should receive a 1000 sats as change",
681 );
682
683 assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
684 assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
685 assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
686 assert_eq!(vtxos[3].policy().user_pubkey(), alice_public_key());
687
688 let funding_map = HashMap::from([
689 (funding_tx_1.compute_txid(), funding_tx_1),
690 (funding_tx_2.compute_txid(), funding_tx_2),
691 (funding_tx_3.compute_txid(), funding_tx_3),
692 ]);
693 verify_package_builder(
694 package, &[alice_keypair(), alice_keypair(), alice_keypair()], funding_map,
695 );
696 }
697
698 #[test]
699 fn can_send_multiple_vtxos_with_dust_change() {
700 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
704 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(1_000));
705
706 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
707 [alice_vtxo_1, alice_vtxo_2],
708 ArkoorDestination {
709 total_amount: Amount::from_sat(5_700),
710 policy: VtxoPolicy::new_pubkey(bob_public_key()),
711 },
712 VtxoPolicy::new_pubkey(alice_public_key())
713 ).expect("Valid package");
714
715 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
716 assert_eq!(vtxos.len(), 4);
717 assert_eq!(vtxos[0].amount(), Amount::from_sat(5_000));
718 assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
719 assert_eq!(vtxos[1].amount(), Amount::from_sat(670));
720 assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
721 assert_eq!(vtxos[2].amount(), Amount::from_sat(30));
722 assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
723 assert_eq!(vtxos[3].amount(), Amount::from_sat(300));
724 assert_eq!(vtxos[3].policy().user_pubkey(), alice_public_key());
725 }
726
727 #[test]
728 fn not_enough_money() {
729 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(900));
733 let result = ArkoorPackageBuilder::new_single_output_with_checkpoints(
734 [alice_vtxo],
735 ArkoorDestination {
736 total_amount: Amount::from_sat(1000),
737 policy: VtxoPolicy::new_pubkey(bob_public_key()),
738 },
739 VtxoPolicy::new_pubkey(alice_public_key())
740 );
741
742 match result {
743 Ok(_) => panic!("Package should be invalid"),
744 Err(ArkoorConstructionError::Unbalanced { input, output }) => {
745 assert_eq!(input, Amount::from_sat(900));
746 assert_eq!(output, Amount::from_sat(1000));
747 }
748 Err(e) => panic!("Unexpected error: {:?}", e),
749 }
750 }
751
752 #[test]
753 fn not_enough_money_with_multiple_inputs() {
754 let (_funding_tx, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
758 let (_funding_tx, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
759 let (_funding_tx, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
760
761 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
762 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
763 ArkoorDestination {
764 total_amount: Amount::from_sat(20_000),
765 policy: VtxoPolicy::new_pubkey(bob_public_key()),
766 },
767 VtxoPolicy::new_pubkey(alice_public_key())
768 );
769
770 match package {
771 Ok(_) => panic!("Package should be invalid"),
772 Err(ArkoorConstructionError::Unbalanced { input, output }) => {
773 assert_eq!(input, Amount::from_sat(17_000));
774 assert_eq!(output, Amount::from_sat(20_000));
775 }
776 Err(e) => panic!("Unexpected error: {:?}", e)
777 }
778 }
779
780 #[test]
781 fn can_use_all_provided_inputs_with_change() {
782 let (_funding_tx, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
787 let (_funding_tx, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(1000));
788 let (_funding_tx, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(1000));
789 let (_funding_tx, alice_vtxo_4) = dummy_vtxo_for_amount(Amount::from_sat(1000));
790
791 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
792 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3, alice_vtxo_4],
793 ArkoorDestination {
794 total_amount: Amount::from_sat(2000),
795 policy: VtxoPolicy::new_pubkey(bob_public_key()),
796 },
797 VtxoPolicy::new_pubkey(alice_public_key())
798 ).expect("Package should be valid");
799
800 let vtxos = package.build_unsigned_vtxos().collect::<Vec<_>>();
802 let total_output = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
803 assert_eq!(total_output, Amount::from_sat(4000));
804 }
805
806 #[test]
807 fn single_input_multiple_outputs() {
808 let (funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
810
811 let outputs = vec![
812 ArkoorDestination {
813 total_amount: Amount::from_sat(4_000),
814 policy: VtxoPolicy::new_pubkey(bob_public_key())
815 },
816 ArkoorDestination {
817 total_amount: Amount::from_sat(3_000),
818 policy: VtxoPolicy::new_pubkey(bob_public_key())
819 },
820 ArkoorDestination {
821 total_amount: Amount::from_sat(3_000),
822 policy: VtxoPolicy::new_pubkey(bob_public_key())
823 },
824 ];
825
826 let package = ArkoorPackageBuilder::new_with_checkpoints(
827 [alice_vtxo.clone()],
828 outputs,
829 ).expect("Valid package");
830
831 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
832 assert_eq!(vtxos.len(), 3);
833 assert_eq!(vtxos[0].amount(), Amount::from_sat(4_000));
834 assert_eq!(vtxos[1].amount(), Amount::from_sat(3_000));
835 assert_eq!(vtxos[2].amount(), Amount::from_sat(3_000));
836
837 let user_keypair = alice_keypair();
839 let user_builder = package.generate_user_nonces(&[user_keypair])
840 .expect("Valid nb of keypairs");
841 let cosign_requests = user_builder.cosign_request();
842
843 let cosign_responses = ArkoorPackageBuilder::from_cosign_request(cosign_requests)
844 .expect("Invalid cosign requests")
845 .server_cosign(&server_keypair())
846 .expect("Wrong server key")
847 .cosign_response();
848
849 let signed_vtxos = user_builder.user_cosign(&[user_keypair], cosign_responses)
850 .expect("Invalid cosign responses")
851 .build_signed_vtxos();
852
853 assert_eq!(signed_vtxos.len(), 3, "Should create 3 signed vtxos");
854
855 signed_vtxos[0].validate(&funding_tx).expect("First vtxo should be valid");
857 }
858
859 #[test]
860 fn output_split_across_inputs() {
861 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(600));
864 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
865
866 let outputs = vec![
867 ArkoorDestination {
868 total_amount: Amount::from_sat(800),
869 policy: VtxoPolicy::new_pubkey(bob_public_key())
870 },
871 ArkoorDestination {
872 total_amount: Amount::from_sat(300),
873 policy: VtxoPolicy::new_pubkey(bob_public_key())
874 },
875 ];
876
877 let package = ArkoorPackageBuilder::new_with_checkpoints(
878 [alice_vtxo_1, alice_vtxo_2],
879 outputs,
880 ).expect("Valid package");
881
882 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
883 assert_eq!(vtxos.len(), 3);
884 assert_eq!(vtxos[0].amount(), Amount::from_sat(600));
885 assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
886 assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
887 assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
888 assert_eq!(vtxos[2].amount(), Amount::from_sat(300));
889 assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
890 }
891
892 #[test]
893 fn dust_splits_allowed() {
894 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(500));
897 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
898
899 let outputs = vec![
900 ArkoorDestination {
901 total_amount: Amount::from_sat(750),
902 policy: VtxoPolicy::new_pubkey(bob_public_key())
903 },
904 ArkoorDestination {
905 total_amount: Amount::from_sat(250),
906 policy: VtxoPolicy::new_pubkey(bob_public_key())
907 },
908 ];
909
910 let package = ArkoorPackageBuilder::new_with_checkpoints(
911 [alice_vtxo_1, alice_vtxo_2],
912 outputs,
913 ).expect("Valid package");
914
915 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
916 assert_eq!(vtxos.len(), 3);
917 assert_eq!(vtxos[0].amount(), Amount::from_sat(500));
918 assert_eq!(vtxos[1].amount(), Amount::from_sat(250)); assert_eq!(vtxos[2].amount(), Amount::from_sat(250));
920 }
921
922 #[test]
923 fn unbalanced_amounts_rejected() {
924 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
926
927 let outputs = vec![
928 ArkoorDestination {
929 total_amount: Amount::from_sat(600),
930 policy: VtxoPolicy::new_pubkey(bob_public_key())
931 },
932 ArkoorDestination {
933 total_amount: Amount::from_sat(600),
934 policy: VtxoPolicy::new_pubkey(bob_public_key())
935 },
936 ];
937
938 let result = ArkoorPackageBuilder::new_with_checkpoints(
939 [alice_vtxo],
940 outputs,
941 );
942
943 match result {
944 Err(ArkoorConstructionError::Unbalanced { input, output }) => {
945 assert_eq!(input, Amount::from_sat(1000));
946 assert_eq!(output, Amount::from_sat(1200));
947 }
948 _ => panic!("Expected Unbalanced error"),
949 }
950 }
951
952 #[test]
953 fn empty_outputs_rejected() {
954 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
955
956 let result = ArkoorPackageBuilder::new_with_checkpoints(
957 [alice_vtxo],
958 vec![],
959 );
960
961 match result {
962 Err(ArkoorConstructionError::NoOutputs) => {}
963 Err(e) => panic!("Expected NoOutputs error, got: {:?}", e),
964 Ok(_) => panic!("Expected NoOutputs error, got Ok"),
965 }
966 }
967
968 #[test]
969 fn multiple_inputs_multiple_outputs_exact_balance() {
970 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
972 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(2000));
973 let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(1500));
974
975 let outputs = vec![
976 ArkoorDestination {
977 total_amount: Amount::from_sat(2500),
978 policy: VtxoPolicy::new_pubkey(bob_public_key())
979 },
980 ArkoorDestination {
981 total_amount: Amount::from_sat(2000),
982 policy: VtxoPolicy::new_pubkey(bob_public_key())
983 },
984 ];
985
986 let package = ArkoorPackageBuilder::new_with_checkpoints(
987 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
988 outputs,
989 ).expect("Valid package");
990
991 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
992 assert_eq!(vtxos.len(), 4);
993 assert_eq!(vtxos[0].amount(), Amount::from_sat(1000));
997 assert_eq!(vtxos[1].amount(), Amount::from_sat(1500));
998 assert_eq!(vtxos[2].amount(), Amount::from_sat(500));
999 assert_eq!(vtxos[3].amount(), Amount::from_sat(1500));
1000 }
1001
1002 #[test]
1003 fn single_output_across_many_inputs() {
1004 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(100));
1007 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(100));
1008 let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(100));
1009 let (_funding_tx_4, alice_vtxo_4) = dummy_vtxo_for_amount(Amount::from_sat(100));
1010
1011 let outputs = vec![
1012 ArkoorDestination {
1013 total_amount: Amount::from_sat(400),
1014 policy: VtxoPolicy::new_pubkey(bob_public_key())
1015 },
1016 ];
1017
1018 let package = ArkoorPackageBuilder::new_with_checkpoints(
1019 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3, alice_vtxo_4],
1020 outputs,
1021 ).expect("Valid package");
1022
1023 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1024 assert_eq!(vtxos.len(), 4);
1025 assert_eq!(vtxos[0].amount(), Amount::from_sat(100));
1026 assert_eq!(vtxos[1].amount(), Amount::from_sat(100));
1027 assert_eq!(vtxos[2].amount(), Amount::from_sat(100));
1028 assert_eq!(vtxos[3].amount(), Amount::from_sat(100));
1029 let total: Amount = vtxos.iter().map(|v| v.amount()).sum();
1030 assert_eq!(total, Amount::from_sat(400));
1031 }
1032
1033 #[test]
1034 fn many_outputs_from_single_input() {
1035 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
1037
1038 let outputs = vec![
1039 ArkoorDestination {
1040 total_amount: Amount::from_sat(100),
1041 policy: VtxoPolicy::new_pubkey(bob_public_key())
1042 },
1043 ArkoorDestination {
1044 total_amount: Amount::from_sat(200),
1045 policy: VtxoPolicy::new_pubkey(bob_public_key())
1046 },
1047 ArkoorDestination {
1048 total_amount: Amount::from_sat(150),
1049 policy: VtxoPolicy::new_pubkey(bob_public_key())
1050 },
1051 ArkoorDestination {
1052 total_amount: Amount::from_sat(250),
1053 policy: VtxoPolicy::new_pubkey(bob_public_key())
1054 },
1055 ArkoorDestination {
1056 total_amount: Amount::from_sat(300),
1057 policy: VtxoPolicy::new_pubkey(bob_public_key())
1058 },
1059 ];
1060
1061 let package = ArkoorPackageBuilder::new_with_checkpoints(
1062 [alice_vtxo],
1063 outputs,
1064 ).expect("Valid package");
1065
1066 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1067 assert_eq!(vtxos.len(), 5);
1068 assert_eq!(vtxos[0].amount(), Amount::from_sat(100));
1069 assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
1070 assert_eq!(vtxos[2].amount(), Amount::from_sat(150));
1071 assert_eq!(vtxos[3].amount(), Amount::from_sat(250));
1072 assert_eq!(vtxos[4].amount(), Amount::from_sat(300));
1073 }
1074
1075 #[test]
1076 fn first_input_exactly_matches_first_output() {
1077 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
1080 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
1081
1082 let outputs = vec![
1083 ArkoorDestination {
1084 total_amount: Amount::from_sat(1000),
1085 policy: VtxoPolicy::new_pubkey(bob_public_key())
1086 },
1087 ArkoorDestination {
1088 total_amount: Amount::from_sat(500),
1089 policy: VtxoPolicy::new_pubkey(bob_public_key())
1090 },
1091 ];
1092
1093 let package = ArkoorPackageBuilder::new_with_checkpoints(
1094 [alice_vtxo_1, alice_vtxo_2],
1095 outputs,
1096 ).expect("Valid package");
1097
1098 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1099 assert_eq!(vtxos.len(), 2);
1100 assert_eq!(vtxos[0].amount(), Amount::from_sat(1000));
1101 assert_eq!(vtxos[1].amount(), Amount::from_sat(500));
1102 }
1103
1104 #[test]
1105 fn empty_inputs_rejected() {
1106 let outputs = vec![
1108 ArkoorDestination {
1109 total_amount: Amount::from_sat(1000),
1110 policy: VtxoPolicy::new_pubkey(bob_public_key())
1111 },
1112 ];
1113
1114 let result = ArkoorPackageBuilder::new_with_checkpoints(
1115 Vec::<Vtxo<Full>>::new(),
1116 outputs,
1117 );
1118
1119 match result {
1120 Ok(_) => panic!("Should reject empty inputs"),
1121 Err(ArkoorConstructionError::Unbalanced { input, output }) => {
1122 assert_eq!(input, Amount::ZERO);
1123 assert_eq!(output, Amount::from_sat(1000));
1124 }
1125 Err(e) => panic!("Unexpected error: {:?}", e),
1126 }
1127 }
1128
1129 #[test]
1130 fn alternating_split_pattern() {
1131 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(300));
1136 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(700));
1137 let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(500));
1138
1139 let outputs = vec![
1140 ArkoorDestination {
1141 total_amount: Amount::from_sat(500),
1142 policy: VtxoPolicy::new_pubkey(bob_public_key())
1143 },
1144 ArkoorDestination {
1145 total_amount: Amount::from_sat(400),
1146 policy: VtxoPolicy::new_pubkey(bob_public_key())
1147 },
1148 ArkoorDestination {
1149 total_amount: Amount::from_sat(600),
1150 policy: VtxoPolicy::new_pubkey(bob_public_key())
1151 },
1152 ];
1153
1154 let package = ArkoorPackageBuilder::new_with_checkpoints(
1155 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
1156 outputs,
1157 ).expect("Valid package");
1158
1159 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1160 assert_eq!(vtxos.len(), 5);
1161 assert_eq!(vtxos[0].amount(), Amount::from_sat(300));
1163 assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
1165 assert_eq!(vtxos[2].amount(), Amount::from_sat(400));
1166 assert_eq!(vtxos[3].amount(), Amount::from_sat(100));
1167 assert_eq!(vtxos[4].amount(), Amount::from_sat(500));
1169 let total: Amount = vtxos.iter().map(|v| v.amount()).sum();
1170 assert_eq!(total, Amount::from_sat(1500));
1171 }
1172
1173 #[test]
1174 fn spend_info_correctness_simple_checkpoint() {
1175 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
1177 Amount::from_sat(100_000)
1178 );
1179 let input_id = alice_vtxo.id();
1180
1181 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
1182 [alice_vtxo],
1183 ArkoorDestination {
1184 total_amount: Amount::from_sat(100_000),
1185 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1186 },
1187 VtxoPolicy::new_pubkey(alice_public_key())
1188 ).expect("Valid package");
1189
1190 let internal_vtxos: Vec<VtxoId> = package
1192 .build_unsigned_internal_vtxos()
1193 .iter().map(|(v, _)| v.id())
1194 .collect();
1195
1196 let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1198
1199 let mut expected_vtxo_ids = vec![input_id];
1201 expected_vtxo_ids.extend(internal_vtxos.iter());
1202
1203 let actual_vtxo_ids: Vec<VtxoId> = spend_info
1204 .iter()
1205 .map(|(id, _)| *id)
1206 .collect();
1207
1208 for id in &expected_vtxo_ids {
1210 assert!(
1211 actual_vtxo_ids.contains(id),
1212 "Expected VTXO ID {} not found in spend_info",
1213 id
1214 );
1215 }
1216
1217 assert_eq!(
1219 actual_vtxo_ids.len(),
1220 expected_vtxo_ids.len(),
1221 "spend_info contains unexpected entries"
1222 );
1223 }
1224
1225 #[test]
1226 fn spend_info_correctness_with_dust_isolation() {
1227 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
1229 Amount::from_sat(1000)
1230 );
1231 let input_id = alice_vtxo.id();
1232
1233 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
1234 [alice_vtxo],
1235 ArkoorDestination {
1236 total_amount: Amount::from_sat(900),
1237 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1238 },
1239 VtxoPolicy::new_pubkey(alice_public_key())
1240 ).expect("Valid package");
1241
1242 let internal_vtxos: Vec<VtxoId> = package
1244 .build_unsigned_internal_vtxos()
1245 .iter().map(|(v, _)| v.id())
1246 .collect();
1247
1248 let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1250
1251 let mut expected_vtxo_ids = vec![input_id];
1253 expected_vtxo_ids.extend(internal_vtxos.iter());
1254
1255 let actual_vtxo_ids: Vec<VtxoId> = spend_info
1256 .iter()
1257 .map(|(id, _)| *id)
1258 .collect();
1259
1260 for id in &expected_vtxo_ids {
1262 assert!(
1263 actual_vtxo_ids.contains(id),
1264 "Expected VTXO ID {} not found in spend_info",
1265 id
1266 );
1267 }
1268
1269 assert_eq!(
1271 actual_vtxo_ids.len(),
1272 expected_vtxo_ids.len(),
1273 "spend_info contains unexpected entries"
1274 );
1275 }
1276
1277 #[test]
1278 fn spend_info_correctness_without_checkpoints() {
1279 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
1281 Amount::from_sat(100_000)
1282 );
1283 let input_id = alice_vtxo.id();
1284
1285 let package = ArkoorPackageBuilder::new_without_checkpoints(
1286 [alice_vtxo],
1287 vec![
1288 ArkoorDestination {
1289 total_amount: Amount::from_sat(100_000),
1290 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1291 }
1292 ]
1293 ).expect("Valid package");
1294
1295 let internal_vtxos: Vec<VtxoId> = package
1297 .build_unsigned_internal_vtxos()
1298 .iter().map(|(v, _)| v.id())
1299 .collect();
1300
1301 let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1303
1304 let mut expected_vtxo_ids = vec![input_id];
1306 expected_vtxo_ids.extend(internal_vtxos.iter());
1307
1308 let actual_vtxo_ids: Vec<VtxoId> = spend_info
1309 .iter()
1310 .map(|(id, _)| *id)
1311 .collect();
1312
1313 for id in &expected_vtxo_ids {
1315 assert!(
1316 actual_vtxo_ids.contains(id),
1317 "Expected VTXO ID {} not found in spend_info",
1318 id
1319 );
1320 }
1321
1322 assert_eq!(
1324 actual_vtxo_ids.len(),
1325 expected_vtxo_ids.len(),
1326 "spend_info contains unexpected entries"
1327 );
1328 }
1329
1330 #[test]
1331 fn spend_info_correctness_multiple_inputs() {
1332 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(
1334 Amount::from_sat(10_000)
1335 );
1336 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(
1337 Amount::from_sat(5_000)
1338 );
1339 let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(
1340 Amount::from_sat(2_000)
1341 );
1342
1343 let input_ids = vec![
1344 alice_vtxo_1.id(),
1345 alice_vtxo_2.id(),
1346 alice_vtxo_3.id(),
1347 ];
1348
1349 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
1350 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
1351 ArkoorDestination {
1352 total_amount: Amount::from_sat(16_000),
1353 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1354 },
1355 VtxoPolicy::new_pubkey(alice_public_key())
1356 ).expect("Valid package");
1357
1358 let internal_vtxos: Vec<VtxoId> = package
1360 .build_unsigned_internal_vtxos()
1361 .iter().map(|(v, _)| v.id())
1362 .collect();
1363
1364 let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1366
1367 let mut expected_vtxo_ids = input_ids.clone();
1369 expected_vtxo_ids.extend(internal_vtxos.iter());
1370
1371 let actual_vtxo_ids: Vec<VtxoId> = spend_info
1372 .iter()
1373 .map(|(id, _)| *id)
1374 .collect();
1375
1376 for id in &expected_vtxo_ids {
1378 assert!(
1379 actual_vtxo_ids.contains(id),
1380 "Expected VTXO ID {} not found in spend_info",
1381 id
1382 );
1383 }
1384
1385 assert_eq!(
1387 actual_vtxo_ids.len(),
1388 expected_vtxo_ids.len(),
1389 "spend_info contains unexpected entries"
1390 );
1391 }
1392}